1 line
218 KiB
HTML
1 line
218 KiB
HTML
<!doctype html> <html lang=en> <head> <meta charset=UTF-8> <meta name=viewport content="width=device-width,initial-scale=1"> <title>Radio Dispatch Panel</title> <link rel=icon type=image/png href=favicon.png id=favicon-icon> <link rel="shortcut icon" type=image/png href=favicon.png id=favicon-shortcut> <script type=module>import{createConsola}from"https://cdn.jsdelivr.net/npm/consola@3.4.2/+esm";window.createConsola=createConsola,window.consolaLoaded=!0</script> <script type=module>if(window.__TAURI__){const{invoke:_}=window.__TAURI__.core,{listen:i}=window.__TAURI__.event;window.tauriInvoke=_,window.tauriListen=i}</script> <script>window.addEventListener("error",r=>{const o=r.error?.message||r.message,e=`${r.filename}:${r.lineno}:${r.colno}`,n=r.error?.stack;if(!window.logger?.warn)return console.error("Global error caught:",o,"at",e),n&&console.error("Stack trace:",n),!1;window.logger.warn("Global error caught:",o,"at",e),n&&window.logger.warn("Stack trace:",n),window.logger.wrapAll&&"function"==typeof window.logger.wrapAll||(console.error("Global error caught:",o,"at",e),n&&console.error("Stack trace:",n))}),window.addEventListener("unhandledrejection",r=>{const o=r.reason;window.logger?.warn?(window.logger.warn("Unhandled promise rejection:",o),window.logger.wrapAll&&"function"==typeof window.logger.wrapAll||console.warn("Unhandled promise rejection:",o)):console.warn("Unhandled promise rejection:",o)})</script> <link rel=stylesheet href=https://cdn.jsdelivr.net/npm/franken-ui@2.1.2/dist/css/core.min.css> <link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel=stylesheet> <link rel=stylesheet href=https://cdn.jsdelivr.net/npm/@unocss/reset/tailwind.min.css> <script>window.__unocss={theme:{fontFamily:{sans:["Poppins","ui-sans-serif","system-ui","sans-serif"],mono:["JetBrains Mono","Fira Code","ui-monospace","monospace"],inter:["Inter","ui-sans-serif","system-ui","sans-serif"]},colors:{background:"hsl(var(--background))",foreground:"hsl(var(--foreground))",muted:"hsl(var(--muted))","muted-foreground":"hsl(var(--muted-foreground))",card:"hsl(var(--card))","card-foreground":"hsl(var(--card-foreground))",popover:"hsl(var(--popover))","popover-foreground":"hsl(var(--popover-foreground))",border:"hsl(var(--border))",input:"hsl(var(--input))",primary:"hsl(var(--primary))","primary-foreground":"hsl(var(--primary-foreground))",secondary:"hsl(var(--secondary))","secondary-foreground":"hsl(var(--secondary-foreground))",accent:"hsl(var(--accent))","accent-foreground":"hsl(var(--accent-foreground))",destructive:"hsl(var(--destructive))","destructive-foreground":"hsl(var(--destructive-foreground))",ring:"hsl(var(--ring))","chart-1":"hsl(var(--chart-1))","chart-2":"hsl(var(--chart-2))","chart-3":"hsl(var(--chart-3))","chart-4":"hsl(var(--chart-4))","chart-5":"hsl(var(--chart-5))"},borderRadius:{DEFAULT:"var(--radius)",sm:"calc(var(--radius) - 4px)",md:"calc(var(--radius) - 2px)",lg:"var(--radius)",xl:"calc(var(--radius) + 4px)","2xl":"calc(var(--radius) + 8px)"}}}</script> <script data-cfasync=false src=https://cdn.jsdelivr.net/npm/@unocss/runtime/uno.global.js></script> <script src=https://cdn.jsdelivr.net/npm/sortablejs@1.15.7/Sortable.min.js></script> <script src=https://cdn.socket.io/4.8.3/socket.io.min.js></script> <style>[un-cloak]{display:none}::-webkit-scrollbar{width:6px;height:6px;position:absolute}::-webkit-scrollbar-track{background:0 0;width:0}::-webkit-scrollbar-thumb{background:rgba(255,255,255,.5);border-radius:3px;border:none;min-height:20px}::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.7)}::-webkit-scrollbar-thumb:active{background:rgba(255,255,255,.9)}*{scrollbar-width:thin;scrollbar-color:rgba(255,255,255,0.5) transparent}@keyframes pulse{0%,100%{opacity:1}50%{opacity:.5}}@keyframes panic-flash{0%,100%{border-color:hsl(var(--destructive));background-color:hsl(var(--destructive) / .2)}50%{border-color:hsl(var(--destructive) / .8);background-color:hsl(var(--destructive) / .3)}}.signal-indicator{animation:pulse 2s infinite}.panic-user{animation:panic-flash 1s infinite}.panic-user-flash{animation:panic-flash 1s infinite}.zone-container.collapsed .zone-content{display:none}.zone-container.collapsed .zone-header{writing-mode:vertical-lr;text-orientation:mixed;height:200px;display:flex;align-items:center;justify-content:center}.channel-container.collapsed .channel-users{display:none}.channel-container.collapsed .channel-details{display:none}.channel-container.collapsed .channel-users{display:none}.channel-container.collapsed .channel-details{display:none}.channel-container.collapsed .channel-header{margin-bottom:0}.dispatch-zone{transition:all .3s ease}.dispatch-zone.collapsed{flex:0 0 auto;min-width:120px;max-width:120px}.sortable-ghost{opacity:.5;background:hsl(var(--muted));border:2px dashed hsl(var(--chart-2));transform:rotate(2deg)}.sortable-chosen{opacity:1;transform:scale(1.05);border:2px solid hsl(var(--chart-2));z-index:1000}.sortable-drag{opacity:.8;transform:rotate(5deg);box-shadow:0 10px 25px hsl(var(--foreground) / .5);z-index:2000}.sortable-fallback{opacity:.8;transform:rotate(5deg);box-shadow:0 10px 25px hsl(var(--foreground) / .5);z-index:2000;cursor:grabbing;background:hsl(var(--card));border:2px solid hsl(var(--chart-2))}.dispatch-user.sortable-chosen,.dispatch-user.sortable-drag,.user-item.sortable-chosen,.user-item.sortable-drag{z-index:100}.channel-users.drag-over{background:hsl(var(--chart-2) / .2);border:2px dashed hsl(var(--chart-2));transition:all .2s ease;box-shadow:inset 0 0 10px hsl(var(--chart-2) / .4)}.uk-dropdown{z-index:10001}.uk-tooltip{z-index:8888}body.dropdown-open .uk-tooltip,body.dropdown-open [uk-tooltip]:hover::after,body.dropdown-open [uk-tooltip]:hover::before{display:none;visibility:hidden;opacity:0;pointer-events:none}body.dropdown-open [title][uk-tooltip]{pointer-events:none}.dispatch-user,.user-item{z-index:1;position:relative}.dispatch-user .uk-inline,.user-item .uk-inline{z-index:10000;position:relative}.dispatch-user.dropdown-active,.user-item.dropdown-active{z-index:9999}.user-item{cursor:grab;transition:all .2s ease;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}.user-item:hover:not(.drag-disabled){transform:translateY(-1px);box-shadow:0 4px 12px hsl(var(--foreground) / .15)}.user-item:active:not(.drag-disabled){cursor:grabbing}.drag-disabled{cursor:not-allowed;opacity:.6}.other-dispatch{cursor:default;opacity:.8}.other-dispatch:hover{cursor:default}.is-dragging .channel-users:not(.drag-over){opacity:.7;transition:all .2s ease}.is-dragging .user-item:not(.sortable-chosen){opacity:.8}.is-dragging .channel-users.drag-over::before{content:"";position:absolute;top:-2px;left:-2px;right:-2px;bottom:-2px;background:linear-gradient(45deg,hsl(var(--chart-2)),transparent,hsl(var(--chart-2)));border-radius:inherit;z-index:-1;animation:dragPulse 1.5s ease-in-out infinite}@keyframes dragPulse{0%,100%{opacity:.3}50%{opacity:.7}}.sortable-chosen .text-sm{font-weight:600}.sortable-ghost .text-sm{font-weight:500;opacity:.7}.ptt-button{transition:all .2s ease;user-select:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;touch-action:manipulation;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;display:flex;align-items:center;justify-content:center;gap:6px;padding:6px 12px;border-radius:.5rem}.ptt-button.active,.ptt-button:active{background:hsl(var(--primary));color:hsl(var(--primary-foreground));transform:scale(.95);box-shadow:0 0 15px hsl(var(--primary) / .5)}.ptt-button.active .h-2,.ptt-button.active .size-4{color:hsl(var(--primary-foreground))}.ptt-button.transmitting{animation:ptt-pulse .5s ease-in-out infinite alternate;background:hsl(var(--primary));border-color:hsl(var(--primary));color:hsl(var(--primary-foreground))}.ptt-button.transmitting .h-2,.ptt-button.transmitting .size-4{color:hsl(var(--primary-foreground))}@media (max-width:768px){.ptt-button{font-size:16px}}.mobile-ptt{font-weight:500}.mobile-ptt:active{transform:scale(.98);background:hsl(var(--chart-2))}.ptt-active-mobile{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}@keyframes ptt-pulse{0%{box-shadow:0 0 10px hsl(var(--primary) / .8)}100%{box-shadow:0 0 20px hsl(var(--primary))}}.dispatch-user .text-sm{font-weight:600}.dispatch-user .text-xs{font-weight:500}.dispatch-user[data-is-my-dispatch=true] .text-sm{color:hsl(var(--chart-1))}.dispatch-user[data-is-my-dispatch=true] .text-xs{color:hsl(var(--chart-1))}.dispatch-user[data-is-my-dispatch=false] .text-sm{color:hsl(var(--chart-5))}.dispatch-user[data-is-my-dispatch=false] .text-xs{color:hsl(var(--chart-5))}.other-dispatch{cursor:not-allowed;user-select:none;background:hsl(var(--chart-5) / .15);border-color:hsl(var(--chart-5) / .6)}.other-dispatch:hover{transform:none}.dispatch-user[data-is-my-dispatch=true]{cursor:grab;background:hsl(var(--chart-1) / .2);border-color:hsl(var(--chart-1))}.dispatch-user[data-is-my-dispatch=true]:hover{transform:translateY(-1px);box-shadow:0 4px 12px hsl(var(--chart-1) / .3);background:hsl(var(--chart-1) / .3)}.dispatch-user[data-is-my-dispatch=true]:active{cursor:grabbing}.sortable-chosen.dispatch-user[data-is-my-dispatch=true]{transform:rotate(2deg);box-shadow:0 8px 25px hsl(var(--chart-1) / .4);background:hsl(var(--chart-1) / .4)}.dispatch-user[data-is-my-dispatch=true].talking{background:hsl(var(--primary) / .6);border-color:hsl(var(--primary));box-shadow:0 0 8px hsl(var(--primary) / .4)}.dispatch-user[data-is-my-dispatch=true].talking .text-sm,.dispatch-user[data-is-my-dispatch=true].talking .text-xs{color:hsl(var(--primary-foreground));font-weight:600}.other-dispatch.talking{background:hsl(var(--primary) / .4);border-color:hsl(var(--primary));box-shadow:0 0 8px hsl(var(--primary) / .3)}.other-dispatch.talking .text-sm,.other-dispatch.talking .text-xs{color:hsl(var(--primary-foreground));font-weight:600}.voice-status{display:inline-flex;align-items:center;gap:4px;font-size:.75rem;font-weight:600;padding:2px 6px;border-radius:4px;margin-left:8px}.voice-status.listening{background:hsl(var(--chart-3));color:hsl(var(--foreground))}.voice-status.transmitting{background:hsl(var(--primary));color:hsl(var(--primary-foreground));font-weight:600}.voice-status.muted{background:hsl(var(--destructive));color:hsl(var(--destructive-foreground))}#broadcast-modal .uk-dropdown{z-index:1060;max-height:200px;overflow-y:auto}#broadcast-modal .relative{position:relative;z-index:1}#broadcast-modal .dropdown-container{position:relative;display:block}#broadcast-modal form{overflow:visible}.uk-modal-dialog{background:0 0}#broadcast-modal .uk-modal-dialog{max-height:90vh;overflow-y:auto}#broadcast-modal .space-y-4>*+*{margin-top:1rem}.uk-tooltip{background:hsl(var(--popover));color:hsl(var(--popover-foreground));border:1px solid hsl(var(--border));border-radius:4px;padding:8px 12px;font-size:12px;max-width:200px}#broadcast-modal .uk-tab{border-bottom:1px solid hsl(var(--border));background:hsl(var(--muted));border-radius:6px 6px 0 0}#broadcast-modal .uk-tab>li>a{color:hsl(var(--muted-foreground));border:none;padding:8px 16px;font-size:.875rem;transition:all .2s ease}#broadcast-modal .uk-tab>li>a:hover{color:hsl(var(--foreground));background:hsl(var(--accent))}#broadcast-modal .uk-tab>li.uk-active>a{color:hsl(var(--foreground));background:hsl(var(--accent));border-bottom:2px solid hsl(var(--chart-2))}.uk-modal{background:hsl(var(--background) / .8)}.uk-notification{background:0 0}.uk-notification-message{background:0 0;border:none}.uk-notification-message-danger{background:0 0;border:none}.uk-notification-message-success{background:0 0;border:none}:root{--background:0 0% 1%;--foreground:0 0% 98%;--muted:0 0% 14.9%;--muted-foreground:0 0% 63.9%;--card:0 0% 8%;--card-foreground:0 0% 98%;--popover:0 0% 8%;--popover-foreground:0 0% 98%;--border:0 0% 14.9%;--input:0 0% 18%;--primary:210 100% 65%;--primary-foreground:0 0% 98%;--secondary:0 0% 14.9%;--secondary-foreground:0 0% 98%;--accent:0 0% 14.9%;--accent-foreground:0 0% 98%;--destructive:0 84% 60%;--destructive-foreground:0 0% 98%;--ring:0 0% 83%;--chart-1:210 100% 65%;--chart-2:160 60% 45%;--chart-3:210 100% 65%;--chart-4:280 65% 60%;--chart-5:340 75% 55%;--radius:0.5rem;--my-dispatch:220 100% 70%;--my-dispatch-bg:220 100% 15%;--other-dispatch:270 100% 75%;--other-dispatch-bg:270 100% 15%;--status-listening:48 96% 60%;--status-transmitting:142 76% 50%;--status-muted:0 84% 60%}[data-theme=dark]{--background:0 0% 1%;--foreground:0 0% 98%;--muted:0 0% 14.9%;--muted-foreground:0 0% 63.9%;--card:0 0% 8%;--card-foreground:0 0% 98%;--popover:0 0% 8%;--popover-foreground:0 0% 98%;--border:0 0% 14.9%;--input:0 0% 18%;--primary:204 77% 47%;--primary-foreground:0 0% 98%;--secondary:0 0% 14.9%;--secondary-foreground:0 0% 98%;--accent:0 0% 14.9%;--accent-foreground:0 0% 98%;--destructive:0 84% 60%;--destructive-foreground:0 0% 98%;--ring:0 0% 83%;--chart-1:204 77% 47%;--chart-2:160 60% 45%;--chart-3:204 77% 47%;--chart-4:280 65% 60%;--chart-5:340 75% 55%;--radius:0.5rem}[data-theme=basic]{--background:0 0% 88%;--foreground:0 0% 0%;--muted:0 0% 78%;--muted-foreground:0 0% 25%;--card:0 0% 75%;--card-foreground:0 0% 0%;--popover:0 0% 75%;--popover-foreground:0 0% 0%;--border:0 0% 50%;--input:0 0% 69%;--primary:240 100% 27%;--primary-foreground:0 0% 100%;--secondary:0 0% 78%;--secondary-foreground:0 0% 0%;--accent:0 0% 69%;--accent-foreground:0 0% 0%;--destructive:0 79% 42%;--destructive-foreground:0 0% 100%;--ring:240 100% 27%;--chart-1:240 100% 27%;--chart-2:120 61% 34%;--chart-3:39 100% 50%;--chart-4:258 76% 52%;--chart-5:348 83% 47%;--radius:0rem}[data-theme=basic-dark]{--background:0 0% 12%;--foreground:0 0% 100%;--muted:0 0% 22%;--muted-foreground:0 0% 75%;--card:0 0% 25%;--card-foreground:0 0% 100%;--popover:0 0% 25%;--popover-foreground:0 0% 100%;--border:0 0% 50%;--input:0 0% 31%;--primary:240 100% 73%;--primary-foreground:0 0% 0%;--secondary:0 0% 22%;--secondary-foreground:0 0% 100%;--accent:0 0% 31%;--accent-foreground:0 0% 100%;--destructive:0 79% 58%;--destructive-foreground:0 0% 0%;--ring:240 100% 73%;--chart-1:240 100% 73%;--chart-2:120 61% 66%;--chart-3:39 100% 50%;--chart-4:258 76% 68%;--chart-5:348 83% 67%;--radius:0rem}[data-theme=dark-blue]{--background:204 50% 0%;--foreground:204 25% 97%;--muted:204 29% 12%;--muted-foreground:204 4% 67%;--popover:204 50% 5%;--popover-foreground:204 25% 97%;--card:204 50% 8%;--card-foreground:204 25% 98%;--border:215 27.9% 16.9%;--input:215 27.9% 16.9%;--primary:204 77% 47%;--primary-foreground:0 0% 100%;--secondary:204 18% 14%;--secondary-foreground:204 18% 74%;--accent:204 25% 19%;--accent-foreground:204 25% 79%;--destructive:15 94% 58%;--destructive-foreground:0 0% 0%;--ring:204 77% 47%;--chart-1:204 77% 47%;--chart-2:204 18% 14%;--chart-3:204 77% 47%;--chart-4:204 18% 17%;--chart-5:204 80% 47%;--radius:0.5rem;--my-dispatch:204 77% 67%;--my-dispatch-bg:204 77% 25%;--other-dispatch:280 70% 70%;--other-dispatch-bg:280 70% 25%;--status-listening:48 96% 60%;--status-transmitting:142 76% 50%;--status-muted:15 94% 58%}[data-theme=dark]{font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace}[data-theme=dark-minimal]{font-family:Poppins,ui-sans-serif,system-ui,sans-serif}[data-theme=dark-blue]{font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace}[data-theme=basic]{font-family:"Courier New","Lucida Console",monospace}[data-theme=basic-dark]{font-family:"Courier New","Lucida Console",monospace}[data-theme=basic] .rounded,[data-theme=basic] .rounded-lg,[data-theme=basic] .rounded-xl{border-radius:0}[data-theme=basic] .shadow-2xl,[data-theme=basic] .shadow-lg,[data-theme=basic] .shadow-md,[data-theme=basic] .shadow-sm,[data-theme=basic] .shadow-xl{box-shadow:none;border:2px outset hsl(var(--border))}[data-theme=basic] .uk-dropdown{border:2px inset hsl(var(--border));box-shadow:none}[data-theme=basic] .uk-btn{border:2px outset hsl(var(--border));border-radius:0}[data-theme=basic] .uk-btn.active,[data-theme=basic] .uk-btn:active{border:2px inset hsl(var(--border))}[data-theme=basic-dark] .rounded,[data-theme=basic-dark] .rounded-lg,[data-theme=basic-dark] .rounded-xl{border-radius:0}[data-theme=basic-dark] .shadow-2xl,[data-theme=basic-dark] .shadow-lg,[data-theme=basic-dark] .shadow-md,[data-theme=basic-dark] .shadow-sm,[data-theme=basic-dark] .shadow-xl{box-shadow:none;border:2px outset hsl(var(--border))}[data-theme=basic-dark] .uk-dropdown{border:2px inset hsl(var(--border));box-shadow:none}[data-theme=basic-dark] .uk-btn{border:2px outset hsl(var(--border));border-radius:0}[data-theme=basic-dark] .uk-btn.active,[data-theme=basic-dark] .uk-btn:active{border:2px inset hsl(var(--border))}:root{font-family:"JetBrains Mono","Fira Code",ui-monospace,monospace}@media (max-width:1200px){.ptt-button{min-width:60px;flex-shrink:0}}.scan-toggle-btn{transition:all .2s ease;background:hsl(var(--secondary));color:hsl(var(--secondary-foreground));border:1px solid hsl(var(--border));font-weight:500;display:flex;align-items:center;justify-content:center;padding:6px 8px;min-width:32px;border-radius:.5rem}.scan-toggle-btn:hover{background:hsl(var(--secondary) / .8);transform:scale(1.05)}.scan-toggle-btn.bg-accent{background:hsl(var(--primary));color:hsl(var(--primary-foreground));box-shadow:0 0 15px hsl(var(--primary) / .5);border-color:hsl(var(--primary))}.hamburger-menu-btn:hover{background:hsl(var(--secondary) / .8)}.scan-transmission-indicator{background:hsl(var(--primary));color:hsl(var(--primary-foreground));padding:2px 8px;border-radius:4px;font-size:10px;font-weight:700;animation:pulse 1.5s infinite;margin-left:8px}@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.7;transform:scale(1.05)}}.uk-input-range-label{display:none}</style> </head> <body class="bg-background text-foreground font-poppins antialiased overflow-x-auto" un-cloak> <script src=https://cdn.jsdelivr.net/npm/franken-ui@2.1.2/dist/js/core.iife.js type=module></script> <script src=https://cdn.jsdelivr.net/npm/franken-ui@2.1.2/dist/js/icon.iife.js type=module></script> <div class="fixed top-0 left-0 right-0 z-50 w-full bg-card border-b border-border shadow-lg overflow-hidden"> <div class="flex h-14 items-center px-4 gap-2 sm:gap-6"> <div class="flex items-center gap-2"> <div class="h-2 w-2 rounded-full bg-primary animate-pulse"></div> <span class="text-sm font-medium text-foreground hidden sm:inline">Radio Dispatch Panel</span> <span id=dispatchSessionIndicator class="text-xs text-chart-1 bg-chart-1/20 px-2 py-1 rounded border border-chart-1/50 hidden"> Session Loading... </span> </div> <div class="flex items-center gap-2 sm:gap-6 text-sm text-muted-foreground overflow-hidden"> <div class="items-center gap-1 hidden xl:flex flex-shrink-0"> <div class=size-4> <uk-icon icon=users></uk-icon> </div> <span>Users:</span> <span id=statusUserCount class="text-foreground font-medium">0</span> </div> <div class="items-center gap-1 hidden xl:flex flex-shrink-0"> <div class=size-4> <uk-icon icon=radio></uk-icon> </div> <span>Channels:</span> <span id=statusChannelCount class="text-foreground font-medium">0</span> </div> <div class="items-center gap-1 hidden xl:flex flex-shrink-0"> <div class=size-4> <uk-icon icon=map></uk-icon> </div> <span>Zones:</span> <span id=statusZoneCount class="text-foreground font-medium">0</span> </div> <div class="flex items-center gap-1 flex-shrink-0"> <div class=size-4> <uk-icon icon=shield-check></uk-icon> </div> <span class="hidden lg:inline text-muted-foreground font-medium">Status:</span> <span id=connectionStatus class="text-primary font-semibold">Connected</span> </div> </div> <div class="ml-auto flex items-center gap-2 sm:gap-4 flex-shrink-0"> <div id=voiceStatus class="flex items-center gap-2 flex-shrink-0"> <span class="text-xs text-muted-foreground hidden lg:inline">Voice:</span> <span id=voiceStatusText class="voice-status muted">MUTED</span> </div> <button type=button class="uk-btn uk-btn-small flex items-center gap-2 px-3 py-2 bg-secondary text-secondary-foreground hover:bg-secondary/80 rounded-lg border border-border" title=Settings data-uk-tooltip="pos: top; cls: uk-active" onclick=openSettingsModal()> <div class=size-4> <uk-icon icon=settings></uk-icon> </div> </button> <button type=button id=pttButton class="ptt-button uk-btn uk-btn-small bg-secondary text-secondary-foreground hover:bg-secondary/80 border border-border select-none" title="Push to Talk - Touch and hold on mobile, click and hold on desktop, or press and hold 'T' key" data-uk-tooltip="pos: top; cls: uk-active" style=touch-action:manipulation;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none onmousedown=startPTT() onmouseup=stopPTT() onmouseleave=stopPTT()> <div class=size-4> <uk-icon icon=mic></uk-icon> </div> <span class="text-sm font-medium">PTT</span> </button> </div> </div> </div> <div class="flex-1 bg-background" style=margin-top:56px> <div id=zonesContainer class="flex min-h-screen" style=min-width:max-content> <div class="flex-1 flex items-center justify-center"> <div class="bg-card border border-border text-card-foreground p-8 rounded-lg shadow-xl max-w-sm"> <div class=text-center> <div class="size-8 mx-auto mb-4 text-chart-1"> <uk-icon icon=loader-2 class=animate-spin></uk-icon> </div> <div class="text-lg font-medium mb-2"> Loading dispatch panel... </div> <div class="text-sm text-muted-foreground"> Connecting to radio system </div> </div> </div> </div> </div> </div> <div id=settings-modal data-uk-modal class=uk-flex-top> <div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical uk-width-1-3@s uk-width-1-4@m"> <div class="bg-card text-card-foreground shadow-2xl p-6 rounded-xl"> <button class="uk-modal-close-default text-muted-foreground hover:text-foreground absolute top-4 right-4 p-2 rounded-lg hover:bg-accent transition-colors" type=button data-uk-close onclick=cancelSettings()></button> <div class=mb-6> <h2 class="uk-modal-title text-foreground text-xl font-semibold flex items-center gap-2"> <div class="size-5 text-chart-1 flex items-center"> <uk-icon icon=settings></uk-icon> </div> Settings </h2> <p class="text-muted-foreground mt-2"> Configure your dispatch panel preferences </p> </div> <form id=settings-form class=space-y-6> <div> <div class="flex items-center justify-between mb-2"> <label class="uk-form-label text-foreground font-medium" for=settings-callsign> Callsign </label> <button type=button onclick=logout() class="flex items-center gap-1.5 px-2 py-1 rounded text-xs text-destructive hover:bg-destructive/10 border border-destructive/30 hover:border-destructive/60 transition-colors" title=Logout> <div class=size-3> <uk-icon icon=log-out></uk-icon> </div> Logout </button> </div> <input class="uk-input bg-input border-border text-foreground placeholder-muted-foreground focus:border-ring w-full p-3" id=settings-callsign placeholder="Enter your callsign..." autocomplete=off> <p class="text-xs text-muted-foreground mt-1"> Update your callsign without re-logging in </p> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=settings-ptt-key> PTT Key </label> <button type=button id=settings-ptt-key class="bg-input/50 hover:bg-input border border-border text-foreground focus:border-ring w-12 h-12 rounded flex items-center justify-center transition-colors font-mono text-lg" onclick=capturePTTKey()> T </button> <p class="text-xs text-muted-foreground mt-1"> Click and press a key to change PTT button </p> </div> <div id=endpoint-setting style=display:none> <label class="uk-form-label text-foreground font-medium block mb-2" for=settings-endpoint> Dispatch Endpoint </label> <input type=url class="uk-input bg-input border-border text-foreground placeholder-muted-foreground focus:border-ring w-full p-3" id=settings-endpoint placeholder=https://dispatch.timmygstudios.com autocomplete=off oninput=validateEndpoint()> <div id=endpoint-validation class="mt-2 hidden"> <div id=endpoint-status class="flex items-center gap-2 text-xs"> <div id=endpoint-icon class=size-4></div> <span id=endpoint-message>Checking endpoint...</span> </div> </div> <p class="text-xs text-muted-foreground mt-1"> Set the dispatch panel URL (desktop app only) </p> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=settings-sfx-volume> SFX Volume <span id=sfx-volume-display class="text-muted-foreground font-normal">(50%)</span> </label> <div class=h-10> <uk-input-range id=settings-sfx-volume name=sfxVolume min=0 max=100 step=1 value=50 label=% label-position=top></uk-input-range> </div> <p class="text-xs text-muted-foreground mt-1"> Volume for tone, background effects, and transmission sounds </p> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=settings-voice-volume> Voice Volume <span id=voice-volume-display class="text-muted-foreground font-normal">(50%)</span> </label> <div class=h-10> <uk-input-range id=settings-voice-volume name=voiceVolume min=0 max=100 step=1 value=50 label=% label-position=top></uk-input-range> </div> <p class="text-xs text-muted-foreground mt-1"> Volume for voice communications </p> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2"> Theme </label> <div class=relative> <button type=button id=settings-theme-btn class="uk-btn bg-input border border-border text-foreground hover:bg-accent w-full text-left px-3 py-2 flex items-center justify-between min-h-[44px]"> Dark <div class=size-4> <uk-icon icon=chevron-down></uk-icon> </div> </button> <div id=settings-theme-dropdown class="uk-drop uk-dropdown min-w-52 bg-popover border border-border shadow-xl rounded-lg mt-1 z-50" data-uk-dropdown="mode: click; target: #settings-theme-btn; boundary: !.relative; boundary-align: true; pos: bottom-left; delay-hide: 0; auto-update: true"> <ul class="uk-nav uk-dropdown-nav p-1"> <li> <button type=button onclick='previewTheme("dark","Dark"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-foreground hover:bg-accent rounded px-3 py-2 block transition-colors"> Dark </button> </li> <li> <button type=button onclick='previewTheme("dark-minimal","Dark Minimal"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-foreground hover:bg-accent rounded px-3 py-2 block transition-colors"> Dark Minimal </button> </li> <li> <button type=button onclick='previewTheme("dark-blue","Dark Blue"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-foreground hover:bg-accent rounded px-3 py-2 block transition-colors"> Dark Blue </button> </li> <li> <button type=button onclick='previewTheme("basic","Basic"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-foreground hover:bg-accent rounded px-3 py-2 block transition-colors"> Basic </button> </li> <li> <button type=button onclick='previewTheme("basic-dark","Basic Dark"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-foreground hover:bg-accent rounded px-3 py-2 block transition-colors"> Basic Dark </button> </li> </ul> </div> </div> <p class="text-xs text-muted-foreground mt-1"> Choose your preferred theme </p> </div> </form> <div class="flex justify-end gap-3 mt-6"> <button class="uk-modal-close uk-btn !bg-secondary text-secondary-foreground !hover:bg-secondary/80 px-4 py-2" type=button onclick=cancelSettings()> Cancel </button> <button class="uk-btn !bg-primary text-primary-foreground !hover:bg-primary/90 flex items-center gap-2 px-4 py-2" type=button onclick=saveSettings()> <div class=size-4> <uk-icon icon=save></uk-icon> </div> <span>Save Settings</span> </button> </div> </div> </div> </div> <div id=endpoint-modal class=uk-modal uk-modal="bg-close: false; esc-close: false"> <div class="uk-modal-dialog uk-margin-auto-vertical"> <div class="uk-modal-header bg-card border-b border-border"> <h2 class="uk-modal-title text-foreground font-semibold"> Configure Endpoint </h2> </div> <div class="uk-modal-body bg-card text-foreground"> <div class=space-y-4> <div class="text-center mb-4"> <div class="size-12 mx-auto my-3 text-blue-500"> <uk-icon icon=shield-check width=48 height=48></uk-icon> </div> <p class=text-muted-foreground> Welcome! Please configure your dispatch server endpoint to continue. You can use the default endpoint or specify a custom one. </p> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=endpoint-config-input> Dispatch Server Endpoint </label> <input type=url class="uk-input bg-input border-border text-foreground placeholder-muted-foreground focus:border-ring w-full p-3" id=endpoint-config-input placeholder=https://dispatch.timmygstudios.com autocomplete=off oninput=validateEndpointConfig()> <div id=endpoint-config-validation class="mt-2 hidden"> <div id=endpoint-config-status class="flex items-center gap-2 text-xs"> <div id=endpoint-config-icon class=size-4></div> <span id=endpoint-config-message>Checking endpoint...</span> </div> </div> </div> </div> </div> <div class="uk-modal-footer bg-card border-t border-border p-4"> <div class="flex justify-end gap-3"> <button class="uk-btn !bg-primary text-primary-foreground !hover:bg-primary/90 px-4 py-2" type=button id=apply-endpoint-btn onclick=applyEndpointConfig() disabled=disabled> <uk-icon icon=check class="size-4 mr-2"></uk-icon> Apply & Continue </button> </div> </div> </div> </div> <div id=auth-modal data-uk-modal="bg-close: false; esc-close: false" class=uk-modal> <div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical uk-width-1-3@s uk-width-1-4@m"> <div class="bg-card text-card-foreground shadow-2xl p-6 rounded-xl"> <div class="mb-6 text-center"> <div class="size-12 mx-auto mb-4 text-chart-1 flex items-center justify-center"> <uk-icon icon=shield-check width=48 height=48></uk-icon> </div> <h2 class="text-foreground text-xl font-semibold"> Dispatch Access Control </h2> <p class="text-muted-foreground mt-2"> Enter your Network Access Code to continue </p> </div> <form id=auth-form class=space-y-4 onsubmit="return event.preventDefault(),authenticate(),!1"> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=callsign> Callsign </label> <input class="uk-input bg-input border-border text-foreground placeholder-muted-foreground focus:border-ring w-full p-3" id=callsign placeholder="Enter your callsign..." autocomplete=off> </div> <div id=nac-code-container> <label class="uk-form-label text-foreground font-medium block mb-2" for=nac-code> Network Access Code </label> <input type=password class="uk-input bg-input border-border text-foreground placeholder-muted-foreground focus:border-ring w-full p-3" id=nac-code placeholder="Enter NAC ID..." autocomplete=off> </div> <div id=auth-error class="hidden text-destructive text-sm"> Invalid Network Access Code. Please try again. </div> </form> <div id=auth-buttons class="flex justify-end gap-3 mt-6"> <button id=auth-submit-btn class="uk-btn !bg-primary text-primary-foreground !hover:bg-primary/90 flex items-center gap-2 px-4 py-2 w-full" type=button onclick=isAuthenticating||authenticate()> <div class=size-4> <uk-icon icon=unlock></uk-icon> </div> <span id=auth-submit-text>Access Dispatch</span> </button> <button id=change-endpoint-btn class="uk-btn !bg-primary text-primary-foreground !hover:bg-primary/90 flex items-center gap-2 px-4 py-2 w-full" type=button onclick=showEndpointConfigModal(!1)> <div class=size-4> <uk-icon icon=link></uk-icon> </div> <span>Change Endpoint</span> </button> </div> </div> </div> </div> <div id=broadcast-modal data-uk-modal class=uk-flex-top> <div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical uk-width-1-2@s uk-width-1-3@m"> <div class="bg-card text-card-foreground shadow-2xl p-6 rounded-xl"> <button class="uk-modal-close-default text-muted-foreground hover:text-foreground absolute top-4 right-4 p-2 rounded-lg hover:bg-accent transition-colors" type=button data-uk-close></button> <div class=mb-6> <h2 class="uk-modal-title text-foreground text-xl font-semibold flex items-center gap-2"> <div class="size-5 text-chart-1"> <uk-icon icon=megaphone></uk-icon> </div> Broadcast Alert </h2> </div> <form id=broadcast-form class=space-y-4> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=alert-type> Alert Type </label> <div class="relative mb-2"> <div class=dropdown-container> <button class="uk-btn bg-input border border-border text-foreground hover:bg-accent w-full text-left px-3 py-2 flex items-center justify-between min-h-[44px]" type=button id=alert-type-btn> <span id=alert-type-text>General Alert</span> <div class=size-4> <uk-icon icon=chevron-down></uk-icon> </div> </button> <div class="uk-drop uk-dropdown w-full bg-popover border border-border shadow-xl rounded-lg mt-1 z-50" data-uk-dropdown="mode: click; target: #alert-type-btn; boundary: !.dropdown-container; boundary-align: true; pos: bottom-left; delay-hide: 0; auto-update: true" id=alert-type-dropdown> <ul class="uk-nav uk-dropdown-nav p-1"> <li> <button type=button onclick='selectAlertType("General Alert"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-foreground hover:bg-accent rounded px-3 py-2 block transition-colors"> General Alert <span class="text-xs text-muted-foreground">(Black text, no background)</span> </button> </li> <li> <button type=button onclick='selectAlertType("Information Alert"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-chart-1 hover:bg-accent hover:text-chart-1 rounded px-3 py-2 block transition-colors"> Information Alert <span class="text-xs text-muted-foreground">(Blue background)</span> </button> </li> <li> <button type=button onclick='selectAlertType("Priority Alert"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-yellow-600 hover:bg-accent hover:text-yellow-600 rounded px-3 py-2 block transition-colors"> Priority Alert <span class="text-xs text-muted-foreground">(Yellow background)</span> </button> </li> <li> <button type=button onclick='selectAlertType("Emergency Alert"),event.preventDefault(),event.stopPropagation()' class="w-full text-left text-destructive hover:bg-accent hover:text-destructive rounded px-3 py-2 block transition-colors"> Emergency Alert <span class="text-xs text-muted-foreground">(Red background)</span> </button> </li> </ul> </div> </div> <input type=hidden id=alert-type value="General Alert"> </div> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2"> Alert Tone </label> <ul class=uk-tab data-uk-tab="animation: uk-animation-fade" id=tone-selector> <li class=uk-active data-tone=none> <a href=# class="px-4 py-2 text-sm" data-tone=none>No Tone</a> </li> <li data-tone=ALERT_A> <a href=# class="px-4 py-2 text-sm" data-tone=ALERT_A>Alert A</a> </li> <li data-tone=ALERT_B> <a href=# class="px-4 py-2 text-sm" data-tone=ALERT_B>Alert B</a> </li> <li data-tone=PANIC> <a href=# class="px-4 py-2 text-sm" data-tone=PANIC>Panic</a> </li> </ul> <input type=hidden id=selected-tone value=none> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=alert-message> Message </label> <textarea class="uk-textarea bg-input border-border text-foreground placeholder-muted-foreground focus:border-ring w-full p-3" id=alert-message rows=4 placeholder="Enter broadcast message..."></textarea> </div> <div> <label class="uk-form-label text-foreground font-medium block mb-2">Target Channel</label> <input type=hidden id=target-frequency> <div id=target-channel-name class="text-sm font-medium p-3 bg-muted border border-border rounded text-foreground"> </div> </div> </form> <div class="flex justify-end gap-3 mt-6 pt-4 border-t border-border"> <button class="uk-btn !bg-secondary text-secondary-foreground !hover:bg-secondary/80 uk-modal-close transition-colors px-4 py-2" type=button> Cancel </button> <button class="uk-btn !bg-primary text-primary-foreground !hover:bg-primary/90 flex items-center gap-2 px-4 py-2" type=button onclick=sendBroadcastAlert()> <div class=size-4> <uk-icon icon=send></uk-icon> </div> <span>Send Broadcast</span> </button> </div> </div> </div> </div> <div id=user-alert-modal data-uk-modal class=uk-flex-top> <div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical uk-width-1-3@s uk-width-1-4@m"> <div class="bg-card text-card-foreground shadow-2xl p-6 rounded-xl"> <button class="uk-modal-close-default text-muted-foreground hover:text-foreground absolute top-4 right-4 p-2 rounded-lg hover:bg-accent transition-colors" type=button data-uk-close></button> <div class=mb-6> <h2 class="uk-modal-title text-foreground text-xl font-semibold flex items-center gap-2"> <div class="size-5 text-chart-3"> <uk-icon icon=bell></uk-icon> </div> Send User Alert </h2> <p class="text-muted-foreground mt-2" id=user-alert-target> Send alert to user </p> </div> <form id=user-alert-form class=space-y-4> <div> <label class="uk-form-label text-foreground font-medium block mb-2" for=user-alert-message> Alert Message </label> <textarea class="uk-textarea bg-input border-border text-foreground placeholder-muted-foreground focus:border-ring w-full p-3 resize-none" id=user-alert-message rows=3 placeholder="Enter your alert message..." required></textarea> </div> </form> <div class="flex justify-end gap-3 mt-6"> <button class="uk-modal-close uk-btn !bg-secondary text-secondary-foreground !hover:bg-secondary/80 px-4 py-2" type=button> Cancel </button> <button class="uk-btn !bg-primary text-primary-foreground !hover:bg-primary/80 flex items-center gap-2 px-4 py-2" type=button onclick=sendUserAlert()> <div class=size-4> <uk-icon icon=send></uk-icon> </div> <span>Send Alert</span> </button> </div> </div> </div> </div> <div id=change-callsign-modal data-uk-modal class=uk-flex-top> <div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical uk-width-1-3@s uk-width-1-4@m"> <div class="bg-card text-card-foreground shadow-2xl p-6 rounded-xl"> <div class="mb-6 text-center"> <div class="size-12 mx-auto mb-4 text-primary flex items-center justify-center"> <uk-icon icon=pen-line width=48 height=48></uk-icon> </div> <h2 class="text-foreground text-xl font-semibold"> Change Callsign </h2> <p class="text-muted-foreground mt-2" id=change-callsign-target> Set callsign for user </p> </div> <form onsubmit=event.preventDefault(),confirmChangeCallsign()> <div class=mb-4> <label class="block text-sm font-medium text-foreground mb-2" for=callsign-input> Callsign </label> <input id=callsign-input class="uk-input w-full bg-secondary text-foreground border border-border rounded-lg px-3 py-2" placeholder="Enter callsign (e.g., 2L-319)" maxlength=32> <p class="text-xs text-muted-foreground mt-1"> Leave empty and click Save to reset to the default name. </p> </div> </form> <div class="flex justify-end gap-3 mt-6"> <button class="uk-modal-close uk-btn !bg-secondary text-secondary-foreground !hover:bg-secondary/80 px-4 py-2" type=button> Cancel </button> <button class="uk-btn !bg-primary text-primary-foreground !hover:bg-primary/80 flex items-center gap-2 px-4 py-2" type=button onclick=confirmChangeCallsign()> <div class=size-4> <uk-icon icon=check></uk-icon> </div> <span>Save</span> </button> </div> </div> </div> </div> <div id=disconnect-confirm-modal data-uk-modal class=uk-flex-top> <div class="uk-modal-dialog uk-modal-body uk-margin-auto-vertical uk-width-1-3@s uk-width-1-4@m"> <div class="bg-card text-card-foreground shadow-2xl p-6 rounded-xl"> <div class="mb-6 text-center"> <div class="size-12 mx-auto mb-4 text-destructive flex items-center justify-center"> <uk-icon icon=user-x width=48 height=48></uk-icon> </div> <h2 class="text-foreground text-xl font-semibold"> Disconnect User </h2> <p class="text-muted-foreground mt-2" id=disconnect-confirm-text> Are you sure you want to disconnect this user from the radio system? </p> </div> <div class="flex justify-end gap-3 mt-6"> <button class="uk-modal-close uk-btn !bg-secondary text-secondary-foreground !hover:bg-secondary/80 px-4 py-2" type=button> Cancel </button> <button class="uk-btn !bg-destructive text-destructive-foreground !hover:bg-destructive/90 flex items-center gap-2 px-4 py-2" type=button onclick=confirmDisconnectUser()> <div class=size-4> <uk-icon icon=user-x></uk-icon> </div> <span>Disconnect</span> </button> </div> </div> </div> </div> <script>const DISPATCH_PANEL_VERSION="v4.3";let authMethod="nac";window.logger={level:3,fatal:(...e)=>{e.length>0&&console.error("[FATAL]",...e)},error:(...e)=>{e.length>0&&console.error("[ERROR]",...e)},warn:(...e)=>{e.length>0&&console.warn("[WARN]",...e)},log:(...e)=>{e.length>0&&console.log("[LOG]",...e)},info:(...e)=>{e.length>0&&console.info("[INFO]",...e)},success:(...e)=>{e.length>0&&console.log("[SUCCESS]",...e)},debug:function(...e){e.length>0&&this.level>=4&&console.log("[DEBUG]",...e)},trace:function(...e){e.length>0&&this.level>=5&&console.log("[TRACE]",...e)},withTag:function(e){return this}};let logger=window.logger;function upgradeToConsola(){if(window.createConsola)try{logger=window.createConsola({level:3,fancy:!0,formatOptions:{date:!0,colors:!0,compact:!1}}).withTag("Dispatch"),logger.wrapAll(),logger.debug("Upgraded to consola logger")}catch(e){console.warn("Failed to upgrade to consola:",e)}}if(window.createConsola)upgradeToConsola();else{const e=setInterval(()=>{window.createConsola&&(clearInterval(e),upgradeToConsola())},10);setTimeout(()=>{clearInterval(e)},2e3)}let isTauriApp=!1,isDesktopApp=!1,desktopEndpoint=window.location.origin,_desktopPTTKey="T";const _tauriBaseURL=null,_TAURI_BASE_URL_KEY="tauri_base_url";function ensureTauriLocalURL(){return!1}async function detectDesktopApp(){try{if(window.__TAURI__||window.__TAURI_INTERNALS__)return isTauriApp=!0,isDesktopApp=!0,logger.info("Running in Tauri app via window object"),document.body.setAttribute("data-tauri","true"),document.body.classList.add("tauri-app"),await loadTauriSettings(),window.__TAURI__?.event&&(window.__TAURI__.event.listen("ptt-event",e=>{handleGlobalPTT(e.payload)}),logger.info("Tauri PTT event listeners registered")),!0}catch(e){logger.warn("Error checking Tauri:",e)}return isDesktopApp}function handleGlobalPTT(e){logger.info("Global PTT event received:",e),logger.info("Current PTT state - isPTTActive:",isPTTActive),"keydown"!==e.action||isPTTActive?"keyup"===e.action&&isPTTActive?(logger.info("Global PTT released from desktop app"),stopPTT()):logger.warn("Global PTT event ignored - action:",e.action,"isPTTActive:",isPTTActive):(logger.info("Global PTT activated from desktop app"),startPTT())}async function notifyPTTKeyChange(e){if(isTauriApp&&window.__TAURI__?.core)try{await window.__TAURI__.core.invoke("update_settings",{newSettings:{endpoint:desktopEndpoint,ptt_key:e,ptt_type:settings.pttType||"keyboard",ptt_mouse_button:settings.pttMouseButton||0,has_configured_endpoint:settings.hasConfiguredEndpoint}}),logger.info("PTT binding updated in Tauri backend:",{key:e,type:settings.pttType,mouseButton:settings.pttMouseButton})}catch(e){logger.warn("Failed to update PTT binding in Tauri backend:",e)}}async function notifyEndpointChange(e){if(!isTauriApp||!window.__TAURI__?.core)throw new Error("Tauri API not available");await window.__TAURI__.core.invoke("update_settings",{newSettings:{endpoint:e,ptt_key:settings.pttKey,ptt_type:settings.pttType||"keyboard",ptt_mouse_button:settings.pttMouseButton||0,has_configured_endpoint:settings.hasConfiguredEndpoint}}),logger.info("Endpoint updated in Tauri backend:",e)}async function loadTauriSettings(){if(isTauriApp&&window.__TAURI__?.core)try{const e=await window.__TAURI__.core.invoke("get_settings");desktopEndpoint=e.endpoint,e.ptt_key?(settings.pttKey=e.ptt_key,_desktopPTTKey=e.ptt_key):_desktopPTTKey=settings.pttKey,e.ptt_type&&(settings.pttType=e.ptt_type),void 0!==e.ptt_mouse_button&&(settings.pttMouseButton=e.ptt_mouse_button),void 0!==e.has_configured_endpoint&&(settings.hasConfiguredEndpoint=e.has_configured_endpoint),saveSettingsToStorage(),e.ptt_key&&e.ptt_type||(await window.__TAURI__.core.invoke("update_settings",{newSettings:{endpoint:desktopEndpoint,ptt_key:settings.pttKey,ptt_type:settings.pttType||"keyboard",ptt_mouse_button:settings.pttMouseButton||0,has_configured_endpoint:settings.hasConfiguredEndpoint}}),logger.info("Synced PTT settings from localStorage to Tauri:",{key:settings.pttKey,type:settings.pttType,mouseButton:settings.pttMouseButton})),logger.info("Loaded Tauri settings:",{endpoint:desktopEndpoint,pttKey:settings.pttKey,pttType:settings.pttType,pttMouseButton:settings.pttMouseButton,hasConfiguredEndpoint:settings.hasConfiguredEndpoint})}catch(e){logger.warn("Failed to load Tauri settings:",e),desktopEndpoint=window.location.origin,_desktopPTTKey=settings.pttKey}}function buildAssetUrl(e){if(isDesktopApp&&desktopEndpoint){const t=e.startsWith("/")?e.substring(1):e;return`${desktopEndpoint.replace(/\/$/,"")}/${t}`}return e}function _notifyStatusUpdate(e,t){if(isDesktopApp)try{if(isTauriApp&&window.__TAURI__?.event)try{return void window.__TAURI__.event.emit("webview-message",{type:"status-update",status:e,message:t})}catch(e){logger.warn("Tauri event failed, falling back to postMessage:",e)}window.parent&&window.parent!==window&&window.parent.postMessage({type:"status-update",status:e,message:t},"*")}catch(e){logger.warn("Failed to notify status update to desktop app:",e)}}let settings={callsign:"",pttKey:"T",pttType:"keyboard",pttMouseButton:0,theme:"dark",sfxVolume:50,voiceVolume:50,hasConfiguredEndpoint:!1};const themeConfigs={dark:{name:"Dark",dataAttribute:"dark",fontFamily:"mono",fontFamilyCSS:'"JetBrains Mono", "Fira Code", ui-monospace, monospace'},"dark-minimal":{name:"Dark Minimal",dataAttribute:null,fontFamily:"sans",fontFamilyCSS:'"Poppins", ui-sans-serif, system-ui, sans-serif'},"dark-blue":{name:"Dark Blue",dataAttribute:"dark-blue",fontFamily:"mono",fontFamilyCSS:'"JetBrains Mono", "Fira Code", ui-monospace, monospace'},basic:{name:"Basic",dataAttribute:"basic",fontFamily:"mono",fontFamilyCSS:'"Courier New", "Lucida Console", monospace'},"basic-dark":{name:"Basic Dark",dataAttribute:"basic-dark",fontFamily:"mono",fontFamilyCSS:'"Courier New", "Lucida Console", monospace'}};let pttKeyCapturing=!1,originalTheme=null,originalSettings=null;function loadSettings(){const e=localStorage.getItem("dispatch-settings");e&&(settings={...settings,...JSON.parse(e)}),updateSettingsUI()}function saveSettingsToStorage(){const e={...settings};delete e.endpoint,localStorage.setItem("dispatch-settings",JSON.stringify(e))}let endpointValidationTimeout=null,lastValidatedEndpoint=null,isEndpointValid=!1,endpointConfigValidationTimeout=null,isEndpointConfigValid=!1;function validateEndpoint(){const e=document.getElementById("settings-endpoint"),t=document.getElementById("endpoint-validation"),n=document.getElementById("endpoint-status"),i=document.getElementById("endpoint-icon"),a=document.getElementById("endpoint-message");if(!e||!isDesktopApp)return;const o=e.value.trim();if(endpointValidationTimeout&&clearTimeout(endpointValidationTimeout),!o)return t.classList.add("hidden"),void(isEndpointValid=!1);t.classList.remove("hidden"),n.className="flex items-center gap-2 text-xs text-muted-foreground",i.innerHTML='<uk-icon icon="loader" class="animate-spin"></uk-icon>',a.textContent="Checking endpoint...",isEndpointValid=!1,endpointValidationTimeout=setTimeout(async()=>{await performEndpointValidation(o)},1500)}async function performEndpointValidation(e){document.getElementById("endpoint-validation");const t=document.getElementById("endpoint-status"),n=document.getElementById("endpoint-icon"),i=document.getElementById("endpoint-message");try{new URL(e);const a=`${e.replace(/\/$/,"")}/api/health`,o=new AbortController,s=setTimeout(()=>o.abort(),5e3),r=await fetch(a,{method:"GET",signal:o.signal,mode:"cors"});if(clearTimeout(s),r.ok){const e=await r.json();"ok"===e.status&&"radio-dispatch"===e.service?e.version&&"v4.3"!==e.version?(t.className="flex items-center gap-2 text-xs text-yellow-600",n.innerHTML='<uk-icon icon="triangle-alert"></uk-icon>',i.textContent=`Endpoint valid but version mismatch (Panel: v4.3, Server: ${e.version})`,isEndpointValid=!0):(t.className="flex items-center gap-2 text-xs text-green-600",n.innerHTML='<uk-icon icon="check"></uk-icon>',i.textContent="Endpoint is valid and reachable",isEndpointValid=!0):(t.className="flex items-center gap-2 text-xs text-yellow-600",n.innerHTML='<uk-icon icon="triangle-alert"></uk-icon>',i.textContent="Endpoint reachable but not a radio dispatch service",isEndpointValid=!1)}else t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="circle-x"></uk-icon>',i.textContent=`Endpoint returned HTTP ${r.status}`,isEndpointValid=!1}catch(e){"AbortError"===e.name?(t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="clock"></uk-icon>',i.textContent="Endpoint timeout - check URL and network",isEndpointValid=!1):e instanceof TypeError&&e.message.includes("Invalid URL")?(t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="circle-x"></uk-icon>',i.textContent="Invalid URL format",isEndpointValid=!1):(t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="circle-x"></uk-icon>',i.textContent="Cannot reach endpoint - check URL",isEndpointValid=!1)}lastValidatedEndpoint=e}function validateEndpointConfig(){const e=document.getElementById("endpoint-config-input"),t=document.getElementById("endpoint-config-validation"),n=document.getElementById("endpoint-config-status"),i=document.getElementById("endpoint-config-icon"),a=document.getElementById("endpoint-config-message"),o=document.getElementById("apply-endpoint-btn");if(!e)return;const s=e.value.trim();if(endpointConfigValidationTimeout&&clearTimeout(endpointConfigValidationTimeout),!s)return t.classList.add("hidden"),isEndpointConfigValid=!1,void(o.disabled=!0);t.classList.remove("hidden"),n.className="flex items-center gap-2 text-xs text-muted-foreground",i.innerHTML='<uk-icon icon="loader" class="animate-spin"></uk-icon>',a.textContent="Checking endpoint...",isEndpointConfigValid=!1,o.disabled=!0,endpointConfigValidationTimeout=setTimeout(async()=>{await performEndpointConfigValidation(s)},1500)}async function performEndpointConfigValidation(e){document.getElementById("endpoint-config-validation");const t=document.getElementById("endpoint-config-status"),n=document.getElementById("endpoint-config-icon"),i=document.getElementById("endpoint-config-message"),a=document.getElementById("apply-endpoint-btn");try{new URL(e);const o=`${e.replace(/\/$/,"")}/api/health`,s=new AbortController,r=setTimeout(()=>s.abort(),5e3),c=await fetch(o,{method:"GET",signal:s.signal,mode:"cors"});if(clearTimeout(r),c.ok){const e=await c.json();"ok"===e.status&&"radio-dispatch"===e.service?(e.version&&"v4.3"!==e.version?(t.className="flex items-center gap-2 text-xs text-yellow-600",n.innerHTML='<uk-icon icon="triangle-alert"></uk-icon>',i.textContent=`Endpoint valid but version mismatch (Panel: v4.3, Server: ${e.version})`,isEndpointConfigValid=!0,a.disabled=!1):(t.className="flex items-center gap-2 text-xs text-green-600",n.innerHTML='<uk-icon icon="check"></uk-icon>',i.textContent="Endpoint is valid and reachable",isEndpointConfigValid=!0,a.disabled=!1),e.authMethod&&"discord"===e.authMethod&&(authMethod="discord")):(t.className="flex items-center gap-2 text-xs text-yellow-600",n.innerHTML='<uk-icon icon="triangle-alert"></uk-icon>',i.textContent="Endpoint reachable but not a radio dispatch service",isEndpointConfigValid=!1,a.disabled=!0)}else t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="circle-x"></uk-icon>',i.textContent=`Endpoint returned HTTP ${c.status}`,isEndpointConfigValid=!1,a.disabled=!0}catch(e){"AbortError"===e.name?(t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="clock"></uk-icon>',i.textContent="Endpoint timeout - check URL and network",isEndpointConfigValid=!1,a.disabled=!0):e instanceof TypeError&&e.message.includes("Invalid URL")?(t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="circle-x"></uk-icon>',i.textContent="Invalid URL format",isEndpointConfigValid=!1,a.disabled=!0):(t.className="flex items-center gap-2 text-xs text-red-600",n.innerHTML='<uk-icon icon="circle-x"></uk-icon>',i.textContent="Cannot reach endpoint - check URL",isEndpointConfigValid=!1,a.disabled=!0)}}async function _applyEndpointConfig(){const e=document.getElementById("endpoint-config-input").value.trim();if(e)if(isEndpointConfigValid)try{settings.hasConfiguredEndpoint=!0,saveSettingsToStorage(),isTauriApp&&await notifyEndpointChange(e),desktopEndpoint=e,UIkit.notification({message:"<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='check'></uk-icon></span> Endpoint configured successfully. Reloading...</div>",status:"success",timeout:2e3}),UIkit.modal("#endpoint-modal").hide(),setTimeout(()=>{window.location.reload()},1e3)}catch(e){logger.error("Failed to apply endpoint configuration:",e),UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to apply endpoint configuration.</div>",status:"danger",timeout:3e3})}else UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Please wait for endpoint validation to complete or fix validation errors.</div>",status:"warning",timeout:3e3});else UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Please enter a valid endpoint URL.</div>",status:"warning",timeout:3e3})}function updateSettingsUI(){const e=document.getElementById("settings-callsign"),t=document.getElementById("settings-ptt-key"),n=document.getElementById("settings-endpoint"),i=document.getElementById("settings-theme-btn"),a=document.getElementById("settings-sfx-volume"),o=document.getElementById("settings-voice-volume"),s=document.getElementById("sfx-volume-display"),r=document.getElementById("voice-volume-display");if(e&&(e.value=settings.callsign||document.getElementById("callsign")?.value||""),t&&(t.textContent=settings.pttKey.toUpperCase()),n&&isDesktopApp?(n.value=desktopEndpoint,n.parentElement.style.display="block",setTimeout(()=>validateEndpoint(),100)):n&&(n.parentElement.style.display="none"),i){const e=themeConfigs[settings.theme];i.textContent=e?e.name:settings.theme}a&&a.setAttribute("value",settings.sfxVolume.toString()),o&&o.setAttribute("value",settings.voiceVolume.toString()),s&&(s.textContent=`(${settings.sfxVolume}%)`),r&&(r.textContent=`(${settings.voiceVolume}%)`)}function _openSettingsModal(){originalTheme=settings.theme,originalSettings={...settings},updateSettingsUI(),setupVolumeSliderListeners(),UIkit.util.on("#settings-modal","hide",e=>{"settings-modal"===e.target.id&&null!==originalSettings&&(logger.debug("Modal hiding - cancelling settings"),setTimeout(()=>{null!==originalSettings&&cancelSettings()},50))}),UIkit.modal("#settings-modal").show()}function setupVolumeSliderListeners(){const e=document.getElementById("settings-sfx-volume"),t=document.getElementById("settings-voice-volume"),n=document.getElementById("sfx-volume-display"),i=document.getElementById("voice-volume-display");e&&e.addEventListener("uk-input-range:input",e=>{const t=parseInt(e.target.value,10);settings.sfxVolume=Number.isNaN(t)?50:t,n&&(n.textContent=`(${settings.sfxVolume}%)`),applyVolumeSettings()}),t&&t.addEventListener("uk-input-range:input",e=>{const t=parseInt(e.target.value,10);settings.voiceVolume=Number.isNaN(t)?50:t,i&&(i.textContent=`(${settings.voiceVolume}%)`),applyVolumeSettings()})}function applyVolumeSettings(){try{const e=settings.sfxVolume/100,t=settings.voiceVolume/100;window.dispatchSfxVolume=e,window.dispatchVoiceVolume=t,toneVolume=.1*e,logger.debug(`Applied volume settings - SFX: ${settings.sfxVolume}%, Voice: ${settings.voiceVolume}%, Tone: ${toneVolume}`)}catch(e){logger.error("Error applying volume settings:",e)}}function _capturePTTKey(){if(pttKeyCapturing)return;pttKeyCapturing=!0;const e=document.getElementById("settings-ptt-key");function t(){document.removeEventListener("keydown",n,!0),document.removeEventListener("mousedown",i,!0),pttKeyCapturing=!1}function n(n){n.preventDefault(),n.stopPropagation();const i=n.key.toUpperCase();settings.pttKey=i,settings.pttType="keyboard",settings.pttMouseButton=0,e.textContent=i,e.classList.remove("bg-accent"),e.classList.add("bg-input/50","hover:bg-input"),t()}function i(n){n.preventDefault(),n.stopPropagation();const i={0:"Mouse1 (Left)",1:"Mouse3 (Middle)",2:"Mouse2 (Right)",3:"Mouse4 (Side)",4:"Mouse5 (Side)"}[n.button]||`Mouse${n.button+1}`,a={0:1,1:3,2:2,3:4,4:5}[n.button]||n.button+1;settings.pttKey=i,settings.pttType="mouse",settings.pttMouseButton=a,e.textContent=i,e.classList.remove("bg-accent"),e.classList.add("bg-input/50","hover:bg-input"),t()}e.textContent="...",e.classList.remove("bg-input/50","hover:bg-input"),e.classList.add("bg-accent"),document.addEventListener("keydown",n,!0),document.addEventListener("mousedown",i,!0)}async function _saveSettings(){const e=document.getElementById("settings-callsign"),t=document.getElementById("settings-endpoint"),n=document.getElementById("settings-sfx-volume"),i=document.getElementById("settings-voice-volume"),a=e.value.trim(),o=settings.callsign;if(settings.callsign=a,n){const e=parseInt(n.value,10);settings.sfxVolume=Number.isNaN(e)?50:e}if(i){const e=parseInt(i.value,10);settings.voiceVolume=Number.isNaN(e)?80:e}if(applyVolumeSettings(),isDesktopApp&&t){const e=t.value.trim();if(e&&e!==desktopEndpoint){if(lastValidatedEndpoint!==e||!isEndpointValid)return void UIkit.notification({message:"<div class='flex items-center bg-yellow-500 text-white p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='warning'></uk-icon></span> Please wait for endpoint validation to complete or fix validation errors.</div>",status:"warning",timeout:3e3});logger.info(`Endpoint updated to: ${e}`);try{return await notifyEndpointChange(e),desktopEndpoint=e,UIkit.notification({message:`<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='check'></uk-icon></span> Endpoint updated to ${e}. Reloading...</div>`,status:"success",timeout:2e3}),UIkit.modal("#settings-modal").hide(),void setTimeout(()=>{window.location.reload()},1e3)}catch(e){logger.error("Failed to update endpoint:",e),UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to update endpoint. Please try again.</div>",status:"danger",timeout:3e3})}}}if(isDesktopApp&&settings.pttKey&¬ifyPTTKeyChange(settings.pttKey),saveSettingsToStorage(),settings.callsign&&settings.callsign!==document.getElementById("callsign").value&&(document.getElementById("callsign").value=settings.callsign),a!==o&&a)try{const e=await authenticatedFetch("/dispatch/user/update-callsign",{method:"POST",body:JSON.stringify({callsign:a,userId:dispatchUserId})}),t=await e.json();if(!t.success)return logger.error(`Failed to update callsign: ${t.error}`),void UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to update callsign</div>",status:"danger",timeout:3e3});{logger.info(`Callsign updated to: ${a}`),dispatchUserId&&dispatchData.users?.[dispatchUserId]&&(dispatchData.users[dispatchUserId].name=a),dispatchUserState.name=a,dispatchSessionName=a;const e=document.getElementById("dispatchSessionIndicator");e&&(e.textContent=a),refreshData()}}catch(e){return logger.error("Error updating callsign:",e),void UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Error updating callsign</div>",status:"danger",timeout:3e3})}const s=document.getElementById("settings-theme-btn").textContent;for(const[e,t]of Object.entries(themeConfigs))if(t.name===s){settings.theme=e;break}applyTheme(settings.theme),saveSettingsToStorage(),originalTheme=null,originalSettings=null,UIkit.notification({message:"<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='check'></uk-icon></span> Settings saved successfully</div>",status:"success",timeout:3e3}),UIkit.modal("#settings-modal").hide()}function _previewTheme(e,t){document.getElementById("settings-theme-btn").textContent=t,settings.theme=e,applyTheme(e),UIkit.dropdown("#settings-theme-dropdown").hide()}function _logout(){deleteCookie("dispatch_session"),deleteCookie("dispatch_auth_token"),deleteCookie("dispatch_callsign"),window.location.reload()}function cancelSettings(){if(originalTheme){applyTheme(originalTheme);const e=themeConfigs[originalTheme];e&&(document.getElementById("settings-theme-btn").textContent=e.name)}if(originalSettings){settings.sfxVolume=originalSettings.sfxVolume,settings.voiceVolume=originalSettings.voiceVolume,settings.callsign=originalSettings.callsign,settings.pttKey=originalSettings.pttKey,applyVolumeSettings();const e=document.getElementById("settings-sfx-volume"),t=document.getElementById("settings-voice-volume");if(e){const t=e.querySelector(".uk-input-range-track"),n=e.querySelector(".uk-input-range-knob"),i=e.querySelector(".uk-input-range-label");t&&(t.style.width=`${settings.sfxVolume}%`),n&&(n.style.left=`${settings.sfxVolume}%`),i&&(i.textContent=`${settings.sfxVolume}%`)}if(t){const e=t.querySelector(".uk-input-range-track"),n=t.querySelector(".uk-input-range-knob"),i=t.querySelector(".uk-input-range-label");e&&(e.style.width=`${settings.voiceVolume}%`),n&&(n.style.left=`${settings.voiceVolume}%`),i&&(i.textContent=`${settings.voiceVolume}%`)}updateSettingsUI()}originalTheme=null,originalSettings=null,UIkit.modal("#settings-modal").hide()}function _selectTheme(e,t){settings.theme=e,document.getElementById("settings-theme-btn").textContent=t}function applyTheme(e){const t=themeConfigs[e];if(!t)return void logger.error(`Unknown theme: ${e}`);const n=document.body;Object.keys(themeConfigs).forEach(e=>{themeConfigs[e].dataAttribute&&n.removeAttribute("data-theme")}),t.dataAttribute?n.setAttribute("data-theme",t.dataAttribute):n.removeAttribute("data-theme"),t.fontFamilyCSS&&(n.style.fontFamily=t.fontFamilyCSS),logger.info(`Theme applied: ${t.name}`),updateUnoTheme(e)}function updateUnoTheme(e){if(window.__unocss?.theme){const t=themeConfigs[e];if(t?.fontFamily){const e=window.__unocss.theme.fontFamily[t.fontFamily];e&&(window.__unocss.theme.fontFamily.sans=e)}logger.debug(`UnoCSS theme updated for: ${e}`)}}const scannedChannels=new Set,channelsAutoUnscanned=new Set;let scanAudioMuted=!1;function isChannelMonitored(e){const t=normalizeFrequency(e),n=currentDispatchChannel&&normalizeFrequency(currentDispatchChannel)===t,i=scannedChannels.has(t);return n||i}const dispatchData={config:null,channels:{},users:{},zones:{},panicStatus:{},lastServerTimestamp:null,tones:null,persistentAlerts:{}};let isAuthenticated=!1;const notifiedPanics=new Set,lastUserStates=new Map;let isDragging=!1;const notificationsShown=new Set;let pttReleaseTimer=null;function getDispatchDisplayId(e){if(e<0){return`TNAC: ${Math.abs(e).toString(16).toUpperCase().slice(-4)}`}return`TNAC: ${Date.now().toString().slice(-4)}`}function normalizeFrequency(e){return"string"==typeof e&&(e=parseFloat(e)),Math.round(1e4*e)/1e4}let dispatchSocket=null,dispatchUserId=null,_dispatchInstanceId=null,dispatchSessionName=null,dispatchSessionId=null,dispatchAuthToken=null,currentDispatchChannel=null,isPTTActive=!1,audioContext=null,mediaStream=null,mediaRecorder=null,micNormalizationChain=null;const _audioProcessingInterval=null;let isTransmitting=!1;const _isListening=!1;let toneAudioContext=null,toneVolume=.1;const dispatchUserState={name:"Dispatch",nacId:"DISPATCH",isTalking:!1,isInPanic:!1};let talkingTimeouts=new Map,isInitialized=!1,_zoneUpdatePending=!1,isAuthenticating=!1;function buildImbeUrl(e){return isDesktopApp&&desktopEndpoint?desktopEndpoint.replace(/\/$/,"")+e:e}const IMBE_RATE=8e3,IMBE_SAMPLES_PER_FRAME=160,IMBE_FRAME_VECTOR_WORDS=8,IMBE_FRAME_VECTOR_BYTES=16,IMBE_OUTPUT_GAIN=2.5,IMBE_SCHEDULE_LOOKAHEAD_S=.12;let _imbeRawModule=null,_imbeLoadPromise=null;async function loadDispatchImbeVocoder(){return _imbeRawModule||(_imbeLoadPromise||(_imbeLoadPromise=(async()=>{const e=buildImbeUrl("/radio/imbe_vocoder.wasm"),t=buildImbeUrl("/radio/imbe_vocoder_glue.js"),n=await fetch(e);if(!n.ok)throw new Error(`[DispatchIMBE] Failed to fetch ${e}: ${n.status} ${n.statusText}`);const i=new Uint8Array(await n.arrayBuffer()),a=await fetch(t);if(!a.ok)throw new Error(`[DispatchIMBE] Failed to fetch ${t}: ${a.status} ${a.statusText}`);let o=await a.text();if(o=o.replace(/var\s+_scriptName\s*=\s*import\.meta\.url/g,'var _scriptName = ""'),o=o.replace(/new\s+URL\s*\(\s*["']imbe_vocoder\.wasm["']\s*,\s*import\.meta\.url\s*\)\.href/g,JSON.stringify(e)),o=o.replace(/\bexport\s+default\s+\w+\s*;?/g,""),o.includes("IMBE_VOCODER_STUB")&&!o.includes("async function ImbeVocoder")&&!o.includes("function ImbeVocoder"))throw new Error("[DispatchIMBE] imbe_vocoder_glue.js is the build stub — WASM not compiled yet.");const s=new Function(`${o}\nreturn ImbeVocoder;`)();if("function"!=typeof s)throw new Error("[DispatchIMBE] ImbeVocoder factory not found after evaluating glue.");const r={wasmBinary:i,locateFile:t=>t.endsWith(".wasm")?e:t};return _imbeRawModule=await s(r),logger.info("[DispatchIMBE] WASM loaded and ready"),_imbeRawModule})(),_imbeLoadPromise=_imbeLoadPromise.catch(e=>{throw _imbeLoadPromise=null,e}),_imbeLoadPromise))}class DispatchImbeDecoder{constructor(e){this._m=e,this._fnCreate=e.cwrap("imbe_create","number",[]),this._fnDestroy=e.cwrap("imbe_destroy",null,["number"]),this._fnDecode=e.cwrap("imbe_decode",null,["number","number","number"]);if(this._fvPtr=e._malloc(16),this._pcmPtr=e._malloc(320),!this._fvPtr||!this._pcmPtr)throw new Error("[DispatchIMBE] WASM heap allocation failed for scratch buffers");if(this._handle=this._fnCreate(),this._handle<0)throw new Error("[DispatchIMBE] imbe_create() returned -1 — no free slots");this._queue=Promise.resolve(),this._nextPlayTime=0,this._destroyed=!1}enqueueWork(e){const t=this._queue.then(()=>e()).catch(e=>(logger.warn("[DispatchIMBE] enqueueWork caught:",e),null));return this._queue=t.then(()=>{},()=>{}),t}async _decodeAndPlay(e){if(this._destroyed)return;const t=160*e.length,n=new Float32Array(t);for(let t=0;t<e.length;t++){const i=e[t];let a=!0;for(let e=0;e<8;e++)if(0!==i[e]){a=!1;break}if(a)continue;const o=this._m.HEAP16,s=this._fvPtr>>1;for(let e=0;e<8;e++)o[s+e]=i[e];this._fnDecode(this._handle,this._fvPtr,this._pcmPtr);const r=this._pcmPtr>>1,c=160*t;for(let e=0;e<160;e++){let t=o[r+e]/32767*2.5;t>1?t=1:t<-1&&(t=-1),n[c+e]=t}}let i;const a=audioContext?audioContext.sampleRate:8e3;if(8e3===a)i=audioContext.createBuffer(1,t,8e3),i.copyToChannel(n,0);else try{const e=Math.ceil(t*a/8e3),o=new OfflineAudioContext(1,e,a),s=o.createBuffer(1,t,8e3);s.copyToChannel(n,0);const r=o.createBufferSource();r.buffer=s,r.connect(o.destination),r.start(0),i=await o.startRendering()}catch(e){logger.warn("[DispatchIMBE] Resample failed, using raw 8 kHz:",e),i=audioContext.createBuffer(1,t,8e3),i.copyToChannel(n,0)}if(this._destroyed)return;if(!audioContext)return;const o=audioContext.currentTime;this._nextPlayTime<o&&(this._nextPlayTime=o+.12);const s=getOrCreateRadioFXChain(),r=audioContext.createBufferSource();r.buffer=i;const c=createDispatchP25RfChain(audioContext,s.inputGain);r.connect(c.inputNode);try{r.start(this._nextPlayTime)}catch(e){return void logger.warn("[DispatchIMBE] source.start failed:",e)}this._nextPlayTime+=i.duration}schedulePacket(e){this.enqueueWork(()=>this._decodeAndPlay(e))}destroy(){if(!this._destroyed){this._destroyed=!0;try{this._handle>=0&&this._fnDestroy(this._handle),this._handle=-1,this._fvPtr&&(this._m._free(this._fvPtr),this._fvPtr=0),this._pcmPtr&&(this._m._free(this._pcmPtr),this._pcmPtr=0)}catch(e){}}}}function createDispatchP25RfChain(e,t){const n=e.createGain();n.gain.value=1;const i=e.createBiquadFilter();i.type="highpass",i.frequency.value=300,i.Q.value=.9;const a=e.createBiquadFilter();a.type="peaking",a.frequency.value=800,a.Q.value=1.8,a.gain.value=-4;const o=e.createBiquadFilter();o.type="peaking",o.frequency.value=1800,o.Q.value=1.2,o.gain.value=5;const s=e.createBiquadFilter();s.type="lowpass",s.frequency.value=3800,s.Q.value=.9;const r=e.createDynamicsCompressor();r.threshold.value=-18,r.knee.value=6,r.ratio.value=4,r.attack.value=.002,r.release.value=.08;const c=e.createGain();return c.gain.value=1.15,n.connect(i).connect(a).connect(o).connect(s).connect(r).connect(c).connect(t),{inputNode:n,disconnect(){try{n.disconnect(),c.disconnect()}catch(e){}}}}const IMBE_FLOAT_TO_INT16=32767,IMBE_INPUT_LEVEL_SCALE=.4,IMBE_ENCODER_SILENCE_THRESHOLD=.004;let _encodeAudioContext=null;function getEncodeAudioContext(){return _encodeAudioContext&&"closed"!==_encodeAudioContext.state||(_encodeAudioContext=new(window.AudioContext||window.webkitAudioContext)),"suspended"===_encodeAudioContext.state&&_encodeAudioContext.resume().catch(()=>{}),_encodeAudioContext}const DISPATCH_ENCODER_LOOKAHEAD_FRAMES=2,DISPATCH_OPUS_PREROLL_8K=56;class DispatchImbeEncoder{constructor(e){this._m=e,this._fnCreate=e.cwrap("imbe_create","number",[]),this._fnDestroy=e.cwrap("imbe_destroy",null,["number"]),this._fnEncode=e.cwrap("imbe_encode",null,["number","number","number"]);if(this._pcmPtr=e._malloc(320),this._fvPtr=e._malloc(16),!this._pcmPtr||!this._fvPtr)throw new Error("[DispatchIMBE Enc] WASM heap allocation failed for scratch buffers");if(this._handle=this._fnCreate(),this._handle<0)throw new Error("[DispatchIMBE Enc] imbe_create() returned -1 — no free slots");this._remainder=new Float32Array(0),this._heldOutputs=[],this._coldStartDiscard=2,this._firstChunk=!0,this._dcBlockX=0,this._dcBlockY=0,this._destroyed=!1}async encodeChunks(e){if(this._destroyed)return null;const t=new Blob(e,{type:"audio/webm; codecs=opus"});if(0===t.size)return null;const n=getEncodeAudioContext();let i;try{const e=await t.arrayBuffer();i=await n.decodeAudioData(e)}catch(e){return logger.warn("[DispatchIMBE Enc] decodeAudioData failed:",e),null}const a=this._mixToMono(i);let o,s=await this._resampleTo8k(a,i.sampleRate);if(this._firstChunk){this._firstChunk=!1;const e=Math.min(56,s.length);e>0&&(s=s.subarray(e))}{const e=.995,t=.18,n=.5,i=2.5,a=new Float32Array(s.length);let o=this._dcBlockX,r=this._dcBlockY;for(let t=0;t<s.length;t++){const n=s[t],i=n-o+e*r;a[t]=i,o=n,r=i}this._dcBlockX=o,this._dcBlockY=r;let c=0;for(let e=0;e<a.length;e++)c+=a[e]*a[e];const d=Math.sqrt(c/a.length);if(d>.001){const e=t/d,o=Math.max(n,Math.min(i,e));for(let e=0;e<a.length;e++)a[e]*=o}s=a}0===this._remainder.length?o=s:(o=new Float32Array(this._remainder.length+s.length),o.set(this._remainder,0),o.set(s,this._remainder.length));const r=Math.floor(o.length/160);if(0===r)return this._remainder=o.slice(0),null;this._remainder=o.slice(160*r);const c=[],d=this._m.HEAP16,l=this._pcmPtr>>1,u=this._fvPtr>>1;for(let e=0;e<r;e++){const t=160*e;let n=0;for(let e=0;e<160;e++){const i=o[t+e];n+=i*i}if(Math.sqrt(n/160)<.004){for(let e=0;e<160;e++)d[l+e]=0;this._fnEncode(this._handle,this._pcmPtr,this._fvPtr),c.push(new Int16Array(8));continue}for(let e=0;e<160;e++){let n=.4*o[t+e]*32767;n>32767&&(n=32767),n<-32768&&(n=-32768),d[l+e]=0|n}this._fnEncode(this._handle,this._pcmPtr,this._fvPtr);const i=new Int16Array(8);for(let e=0;e<8;e++)i[e]=d[u+e];c.push(i)}const g=[];for(const e of this._heldOutputs)g.push(e);let p=0;if(this._coldStartDiscard>0){const e=Math.min(this._coldStartDiscard,r);p=e,this._coldStartDiscard-=e}const h=r-p-Math.min(2,r-p);for(let e=p;e<p+h;e++)g.push(c[e]);return this._heldOutputs=c.slice(p+h),0===g.length?null:this._packAndEncode(g)}reset(){this._remainder=new Float32Array(0),this._heldOutputs=[],this._coldStartDiscard=2,this._firstChunk=!0,this._dcBlockX=0,this._dcBlockY=0,!this._destroyed&&this._handle>=0&&(this._fnDestroy(this._handle),this._handle=this._fnCreate())}flush(){if(this._destroyed||0===this._heldOutputs.length)return null;const e=this._m.HEAP16,t=this._pcmPtr>>1;for(let n=0;n<160;n++)e[t+n]=0;for(let e=0;e<2;e++)this._fnEncode(this._handle,this._pcmPtr,this._fvPtr);const n=this._heldOutputs.slice();return this._heldOutputs=[],0===n.length?null:this._packAndEncode(n)}destroy(){if(!this._destroyed){this._destroyed=!0;try{this._handle>=0&&this._fnDestroy(this._handle),this._handle=-1,this._pcmPtr&&(this._m._free(this._pcmPtr),this._pcmPtr=0),this._fvPtr&&(this._m._free(this._fvPtr),this._fvPtr=0)}catch(e){}}}_mixToMono(e){const t=e.numberOfChannels,n=e.length;if(1===t)return e.getChannelData(0).slice();const i=new Float32Array(n),a=1/t;for(let o=0;o<t;o++){const t=e.getChannelData(o);for(let e=0;e<n;e++)i[e]+=t[e]*a}return i}async _resampleTo8k(e,t){if(8e3===t)return e;const n=Math.ceil(8e3*e.length/t);try{const i=new OfflineAudioContext(1,n,8e3),a=i.createBuffer(1,e.length,t);a.copyToChannel(e,0);const o=i.createBufferSource();o.buffer=a,o.connect(i.destination),o.start(0);return(await i.startRendering()).getChannelData(0).slice()}catch(t){return logger.warn("[DispatchIMBE Enc] resample failed:",t),e}}_packAndEncode(e){const t=e.length,n=new ArrayBuffer(4+16*t),i=new DataView(n);i.setUint32(0,t,!0);let a=4;for(let n=0;n<t;n++)for(let t=0;t<8;t++)i.setInt16(a,e[n][t],!0),a+=2;const o=new Uint8Array(n);let s="";for(let e=0;e<o.length;e+=32768)s+=String.fromCharCode.apply(null,o.subarray(e,e+32768));return btoa(s)}}let _dispatchImbeEncoder=null;function getOrCreateDispatchEncoder(){return _dispatchImbeEncoder&&!_dispatchImbeEncoder._destroyed||(_dispatchImbeEncoder=new DispatchImbeEncoder(_imbeRawModule),logger.info("[DispatchIMBE Enc] Encoder created")),_dispatchImbeEncoder}const dispatchP25Decoders=new Map;function getOrCreateP25Decoder(e){if(dispatchP25Decoders.has(e))return dispatchP25Decoders.get(e);dispatchP25Decoders.size>=15&&logger.warn(`[DispatchIMBE] Decoder pool near capacity (${dispatchP25Decoders.size}/16) — next create may fail`);try{const t=new DispatchImbeDecoder(_imbeRawModule);return dispatchP25Decoders.set(e,t),logger.debug(`[DispatchIMBE] Created decoder for serverId=${e}`),t}catch(t){return logger.error(`[DispatchIMBE] Failed to create decoder for serverId=${e}:`,t),null}}function releaseP25Decoder(e){const t=dispatchP25Decoders.get(e);t&&(dispatchP25Decoders.delete(e),t.enqueueWork(()=>{t.destroy(),logger.debug(`[DispatchIMBE] Decoder destroyed (deferred) for serverId=${e}`)}),logger.debug(`[DispatchIMBE] Queued deferred destroy for serverId=${e}`))}function destroyAllP25Decoders(){for(const[e,t]of dispatchP25Decoders.entries())t.destroy(),logger.debug(`[DispatchIMBE] Destroyed decoder for serverId=${e}`);dispatchP25Decoders.clear()}function unpackImbeFrameVectors(e){if(e.byteLength<4)return[];const t=new DataView(e),n=t.getUint32(0,!0);if(0===n)return[];if(n>500)return logger.warn(`[DispatchIMBE] Implausible frame count: ${n}`),[];const i=4+16*n;if(e.byteLength<i)return logger.warn(`[DispatchIMBE] Buffer too small: need ${i} for ${n} frames, got ${e.byteLength}`),[];const a=[];let o=4;for(let e=0;e<n;e++){const e=new Int16Array(8);for(let n=0;n<8;n++)e[n]=t.getInt16(o,!0),o+=2;a.push(e)}return a}async function enqueueImbePacket(e,t){if(!_imbeRawModule)try{await loadDispatchImbeVocoder()}catch(e){return void logger.error("[DispatchIMBE] Failed to load WASM:",e)}let n;try{const e=atob(t),i=new Uint8Array(e.length);for(let t=0;t<e.length;t++)i[t]=e.charCodeAt(t);n=i.buffer}catch(e){return void logger.error("[DispatchIMBE] base64 decode failed:",e)}const i=unpackImbeFrameVectors(n);if(0===i.length)return void logger.debug(`[DispatchIMBE] No frames in packet from serverId=${e}`);const a=getOrCreateP25Decoder(e);a&&a.schedulePacket(i)}const sirenPlayers=new Map,heliPlayers=new Map,recentGunshots=new Map,healthMonitor={lastServerResponse:Date.now(),isHealthy:!0,maxStaleTime:1e4,checkInterval:null,lastDebugLog:Date.now(),serverVersion:null,hasVersionMismatch:!1};async function authenticatedFetch(e,t={}){if(!dispatchSessionId||!dispatchAuthToken)throw new Error("Not authenticated");let n=e;if(isDesktopApp&&desktopEndpoint&&!e.startsWith("http")){const t=e.startsWith("/")?e.substring(1):e;n=`${desktopEndpoint.replace(/\/$/,"")}/${t}`,logger.debug(`Desktop app API call: ${n}`)}const i={"Content-Type":"application/json",Authorization:`Bearer ${dispatchAuthToken}`,"X-Session-ID":dispatchSessionId,...t.headers};return fetch(n,{...t,headers:i})}async function initDispatch(){if(isDesktopApp&&!settings.hasConfiguredEndpoint)return void showEndpointConfigModal(!0);if(isDesktopApp&&desktopEndpoint&&desktopEndpoint!==window.location.origin&&settings.hasConfiguredEndpoint){if(!await checkEndpointHealth())return void showEndpointConfigModal(!1)}settings.callsign&&(document.getElementById("callsign").value=settings.callsign);const e=document.getElementById("change-endpoint-btn");isDesktopApp?document.getElementById("auth-buttons").classList.replace("justify-end","justify-center"):e.style.display="none",UIkit.modal("#auth-modal").show(),setTimeout(()=>{const e=document.getElementById("callsign"),t=document.getElementById("nac-code");e&&t&&(e.value.trim()?t.focus():e.focus())},200)}async function checkEndpointHealth(){try{if(!desktopEndpoint)return!1;const e=`${desktopEndpoint.replace(/\/$/,"")}/api/health`,t=new AbortController,n=setTimeout(()=>t.abort(),3e3),i=await fetch(e,{method:"GET",signal:t.signal,mode:"cors"});if(clearTimeout(n),i.ok){const e=await i.json();if(e.version&&"v4.3"!==e.version?(healthMonitor.serverVersion=e.version,healthMonitor.hasVersionMismatch=!0,logger.warn(`Version mismatch detected - Panel: v4.3, Server: ${e.version}`)):healthMonitor.hasVersionMismatch=!1,e.authMethod&&"discord"===e.authMethod){authMethod="discord";const e=document.getElementById("nac-code-container");e&&(e.style.display="none");const t=document.getElementById("auth-submit-text");t&&(t.innerText="Login with Discord")}return"ok"===e.status&&"radio-dispatch"===e.service}return!1}catch(e){return logger.warn("Endpoint health check failed:",e),!1}}function showEndpointConfigModal(e=!1){const t="localhost"===window.location.hostname||"127.0.0.1"===window.location.hostname?"http://localhost:7777":"https://dispatch.timmygstudios.com",n=document.querySelector("#endpoint-modal .text-muted-foreground");n&&(n.textContent=e?"Welcome! Please configure your dispatch server endpoint to continue. You can use the default endpoint or specify a custom one.":"Unable to connect to the configured endpoint. Please verify or update your dispatch server endpoint to continue.");const i=document.getElementById("endpoint-config-input");i&&(desktopEndpoint&&desktopEndpoint!==window.location.origin&&!e?i.value=desktopEndpoint:i.value=t,i.placeholder=t,setTimeout(()=>validateEndpointConfig(),100)),UIkit.modal("#endpoint-modal").show(),logger.info("Showing endpoint configuration modal",{isFirstTime:e,currentEndpoint:desktopEndpoint})}function updateCallsignFromAuth(){const e=document.getElementById("callsign")?.value?.trim();e&&e!==settings.callsign&&(settings.callsign=e,saveSettingsToStorage())}function resetAuthButton(){const e=document.getElementById("auth-submit-btn");e&&(e.disabled=!1,e.innerHTML='\n <div class="size-4">\n <uk-icon icon="unlock"></uk-icon>\n </div>\n <span>Access Dispatch</span>\n '),isAuthenticating=!1}const errorDiv=document.getElementById("auth-error");async function authenticate(){const e=document.getElementById("nac-code").value;if(isAuthenticating)return;isAuthenticating=!0;const t=document.getElementById("callsign").value,n=document.getElementById("auth-submit-btn");if(n&&(n.disabled=!0,n.innerHTML='\n <div class="size-4">\n <uk-icon icon="loader-2" class="animate-spin"></uk-icon>\n </div>\n <span>Authenticating...</span>\n '),!t.trim())return errorDiv.classList.remove("hidden"),errorDiv.textContent="Please enter a callsign",void resetAuthButton();if(!e.trim()&&"nac"===authMethod)return errorDiv.classList.remove("hidden"),errorDiv.textContent="Please enter a Network Access Code",void resetAuthButton();try{let n="/radio/dispatch/auth";isDesktopApp&&desktopEndpoint&&(n=`${desktopEndpoint.replace(/\/$/,"")}/radio/dispatch/auth`,logger.debug(`Desktop app auth URL: ${n}`));const i={nacId:e,callsign:t.trim()};isTauriApp&&(i.tauriBaseURL=window.location.origin,logger.debug(`Tauri base URL included in auth request: ${i.tauriBaseURL}`));const a=await fetch(n,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(i)});if(a.ok)switch(a.status){case 200:{const e=await a.json();dispatchSessionId=e.sessionId,dispatchAuthToken=e.authToken,isAuthenticated=!0,await postAuthenticate();break}case 202:{const e=await a.json();if(e.redirectUri)return void window.location.replace(e.redirectUri);break}default:throw new Error(`Unexpected authentication response, status: ${a.status}`)}else notificationsShown.clear(),errorDiv.classList.remove("hidden"),errorDiv.textContent="Invalid Network Access Code",document.getElementById("callsign").value="",document.getElementById("nac-code").value="",resetAuthButton()}catch(e){notificationsShown.clear(),logger.error(`Authentication failed: ${e}`),errorDiv.classList.remove("hidden"),errorDiv.textContent="Authentication failed. Please try again.",document.getElementById("callsign").value="",document.getElementById("nac-code").value="",resetAuthButton()}}function persistSessionCookies(e){if(!dispatchSessionId||!dispatchAuthToken)return;const t=encodeURIComponent;document.cookie=`dispatch_session=${t(dispatchSessionId)}; Path=/; Max-Age=86400; SameSite=Strict`,document.cookie=`dispatch_auth_token=${t(dispatchAuthToken)}; Path=/; Max-Age=86400; SameSite=Strict`,document.cookie=`dispatch_callsign=${t(e||"Dispatch")}; Path=/; Max-Age=86400; SameSite=Strict`,logger.debug("Session cookies persisted for refresh recovery")}async function reauthenticateSession(){if(!dispatchAuthToken)return logger.warn("reauthenticateSession: no authToken available, cannot re-auth"),!1;try{logger.info("Re-authenticating session after resource restart...");let e="/radio/dispatch/reauth";isDesktopApp&&desktopEndpoint&&(e=`${desktopEndpoint.replace(/\/$/,"")}/radio/dispatch/reauth`);const t=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${dispatchAuthToken}`},body:JSON.stringify({callsign:dispatchSessionName||"Dispatch"})});if(!t.ok)return logger.error(`Re-auth failed with status ${t.status}`),!1;const n=await t.json();return n.success&&n.sessionId?(dispatchSessionId=n.sessionId,persistSessionCookies(dispatchSessionName||"Dispatch"),dispatchSocket?.connected&&dispatchSocket.emit("setDispatchSession",dispatchSessionId),await requestConfigData(),logger.info("Re-authentication successful — session restored transparently"),!0):(logger.error("Re-auth response missing sessionId"),!1)}catch(e){return logger.error(`Re-authentication error: ${e.message}`),!1}}async function postAuthenticate(){try{if(isInitialized)return void logger.warn("Already initialized, skipping");isInitialized=!0,logger.info("Starting postAuthenticate flow");const e=document.getElementById("callsign"),t=document.getElementById("nac-code"),n=e?e.value:dispatchSessionName||"Dispatch",i=t?t.value:"DISPATCH";UIkit.modal("#auth-modal").hide();const a=document.getElementById("auth-error");a&&a.classList.add("hidden"),dispatchSocket?.connected&&dispatchSessionId&&dispatchSocket.emit("setDispatchSession",dispatchSessionId);for(let e=localStorage.length-1;e>=0;e--){const t=localStorage.key(e);t?.startsWith("dispatchSession")&&localStorage.removeItem(t)}const o=Date.now(),s=i.substring(0,4).toUpperCase();dispatchSessionName=n.trim(),_dispatchInstanceId=`dispatch_${o}_${Math.random().toString(36).substr(2,9)}`,dispatchUserId=-(1e3+o%1e5+Math.floor(1e4*Math.random())+Math.floor(1e3*performance.now())%1e4),logger.info(`✨ Created new dispatch session: ${dispatchSessionName}`),dispatchUserState.name=dispatchSessionName,dispatchUserState.nacId=s,persistSessionCookies(dispatchSessionName),document.title=`Radio Dispatch Panel - ${dispatchSessionName}`;const r=document.getElementById("dispatchSessionIndicator");r.textContent=dispatchSessionName,r.classList.remove("hidden"),await requestConfigData(),notificationsShown.has("loading")||(notificationsShown.add("loading"),UIkit.notification({message:"<div class='flex items-center bg-chart-1 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='loader-2' class='animate-spin'></uk-icon></span>Loading dispatch data...</div>",status:"primary",timeout:3e3})),await refreshData(),setTimeout(async()=>{await refreshData()},50),initHealthMonitoring(),setInterval(refreshData,500),window.addEventListener("focus",()=>{refreshData()}),document.addEventListener("visibilitychange",()=>{document.hidden||(logger.debug("Page became visible, refreshing data"),refreshData())}),updateCallsignFromAuth(),await initDispatchVoice(),setTimeout(()=>{validateAudioState()},1e3),logger.info("Dispatch panel initialized"),setTimeout(()=>{notificationsShown.has("success")||(notificationsShown.add("success"),UIkit.notification({message:"<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='check'></uk-icon></span>Dispatch panel ready!</div>",status:"success",timeout:2e3}))},1e3)}catch(e){logger.error(`Error in postAuthenticate: ${e.message}`),logger.error(`Stack trace: ${e.stack}`),isInitialized=!1,UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span>Failed to initialize dispatch panel. Please refresh the page.</div>",status:"danger",timeout:1e4})}}async function requestConfigData(){try{const e=await authenticatedFetch("/radio/dispatch/config");dispatchData.config=await e.json(),persistentRadioFX&&(logger.info("🔊 Config refreshed — invalidating radio FX chain for rebuild"),persistentRadioFX=null),void 0!==dispatchData.config.logLevel?(logger.level=dispatchData.config.logLevel,logger.info(`Logger level updated to ${dispatchData.config.logLevel} from server config`)):logger.warn("No logLevel found in server config, keeping default level 3"),dispatchData.config||(dispatchData.config={zones:{}}),dispatchData.config.zones||(dispatchData.config.zones={}),document.getElementById("statusZoneCount").textContent=Object.keys(dispatchData.config.zones||{}).length,await requestTonesData(),generateAlertButtons(),renderZoneLayout(),requestAnimationFrame(()=>{updateZoneData()})}catch(e){logger.error(`Failed to get config data: ${e}`),dispatchData.config={zones:{},alerts:{}},renderZoneLayout()}}async function requestTonesData(){try{const e=await authenticatedFetch("/radio/dispatch/tones"),t=await e.json(),n={};for(const[e,i]of Object.entries(t||{}))n[e.toUpperCase()]=i;dispatchData.tones=n,logger.debug("Loaded tones data (keys normalized to uppercase):",Object.keys(dispatchData.tones||{}));const i=Object.keys(dispatchData.tones||{})[0];i&&logger.debug(`Sample tone config for ${i}:`,dispatchData.tones[i]),logger.debug("All available tones for broadcast:",Object.keys(dispatchData.tones||{}))}catch(e){logger.error(`Failed to get tones data: ${e}`),dispatchData.tones={}}}function generateAlertButtons(){if(!dispatchData.config||!dispatchData.config.alerts||0===Object.keys(dispatchData.config.alerts).length)return void logger.warn("No alerts config available for button generation");logger.debug("Alert type dropdown left as-is for standard broadcast alert types");const e=document.getElementById("tone-selector");if(e&&dispatchData.tones){for(;e.firstChild;)e.removeChild(e.firstChild);const t=document.createElement("li");t.className="uk-active",t.dataset.tone="none";const n=document.createElement("a");n.href="#",n.className="px-4 py-2 text-sm",n.dataset.tone="none",n.textContent="No Tone",t.appendChild(n),e.appendChild(t),Object.keys(dispatchData.tones).forEach(t=>{if("PTT"!==t&&"PTT_END"!==t&&"TX_START"!==t&&"TX_END"!==t&&"TX_INTERFERENCE"!==t){const n=document.createElement("li");n.dataset.tone=t;const i=document.createElement("a");i.href="#",i.className="px-4 py-2 text-sm",i.dataset.tone=t;const a=t.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase());i.textContent=a,n.appendChild(i),e.appendChild(n)}}),setTimeout(()=>{const t=UIkit.tab(e,{animation:"uk-animation-fade"});setupToneSelection(e,t)},100)}}function setupToneSelection(e,t){e||(e=document.getElementById("tone-selector")),e&&(e._toneClickHandler&&e.removeEventListener("click",e._toneClickHandler),e._toneClickHandler=e=>{const t=e.target.closest("a[data-tone]"),n=e.target.closest("li[data-tone]"),i=t?.dataset.tone||n?.dataset.tone||"none",a=document.getElementById("selected-tone");a&&(a.value=i,logger.debug(`Tone selection updated (delegated click): ${i}`))},e.addEventListener("click",e._toneClickHandler))}function generateAlertDropdownButtons(e,t){if(!dispatchData.config||!dispatchData.config.alerts||0===Object.keys(dispatchData.config.alerts).length)return`\n <li><a href="#" onclick="openBroadcastModal('${e}', '${t}'); event.preventDefault();" class="text-foreground hover:bg-accent rounded px-3 py-2 flex items-center transition-colors">\n <div class="size-4 mr-2">\n <uk-icon icon="megaphone"></uk-icon>\n </div>\n Broadcast Alert\n </a></li>\n `;let n="";const i=Object.values(dispatchData.config.alerts),a=i.filter(e=>e.isPersistent);a.forEach((t,i)=>{const a=dispatchData.activeAlerts?.[e]&&dispatchData.activeAlerts[e].name===t.name,o=a?`Clear ${t.name}`:`Trigger ${t.name}`,s=a?"shield-check":"alert-triangle";n+=`\n <li><a href="#" onclick="_toggleAlert('${e}', '${t.name}', event); event.preventDefault();" class="hover:bg-accent rounded px-3 py-2 flex items-center transition-colors" style="color: ${t.color||"#dc2626"}" id="alert-toggle-${e}-${i}">\n <div class="size-4 mr-2" style="color: ${t.color||"#dc2626"}">\n <uk-icon icon="${s}"></uk-icon>\n </div>\n ${o}\n </a></li>\n `}),a.length>0&&(n+='<li class="uk-nav-divider border-t border-border my-1"></li>');return i.filter(e=>!e.isPersistent).forEach((t,i)=>{n+=`\n <li><a href="#" onclick="_triggerAlert('${e}', '${t.name}', event); event.preventDefault();" class="hover:bg-accent rounded px-3 py-2 flex items-center transition-colors" style="color: ${t.color||"#059669"}" id="alert-trigger-${e}-${i}">\n <div class="size-4 mr-2" style="color: ${t.color||"#059669"}">\n <uk-icon icon="volume-2"></uk-icon>\n </div>\n ${t.name}\n </a></li>\n `}),i.length>0&&(n+='<li class="uk-nav-divider border-t border-border my-1"></li>'),n+=`\n <li><a href="#" onclick="openBroadcastModal('${e}', '${t}'); event.preventDefault();" class="text-foreground hover:bg-accent rounded px-3 py-2 flex items-center transition-colors">\n <div class="size-4 mr-2">\n <uk-icon icon="megaphone"></uk-icon>\n </div>\n Broadcast Alert\n </a></li>\n `,n}function updateHealthStatus(){const e=document.getElementById("connectionStatus"),t=Date.now()-healthMonitor.lastServerResponse,n=healthMonitor.isHealthy;Math.floor(Date.now()/3e4)!==Math.floor(healthMonitor.lastDebugLog/3e4)&&(healthMonitor.lastDebugLog=Date.now(),logger.debug(`Health check: ${t}ms since last response, healthy: ${healthMonitor.isHealthy}`)),t>healthMonitor.maxStaleTime?(healthMonitor.isHealthy=!1,e&&(e.textContent="Connection Lost",e.className="text-destructive font-medium"),n&&(logger.warn(`Connection health degraded - no server response for ${t}ms (threshold: ${healthMonitor.maxStaleTime}ms)`),logger.debug(`Last successful response was at: ${new Date(healthMonitor.lastServerResponse).toISOString()}`))):(healthMonitor.isHealthy=!0,e&&(healthMonitor.hasVersionMismatch?(e.textContent=`Version Mismatch (Panel: v4.3, Server: ${healthMonitor.serverVersion})`,e.className="text-red-500 font-semibold"):mediaStream?(e.textContent="Connected",e.className="text-primary font-semibold"):(e.textContent="Microphone Access Denied (Listen Only)",e.className="text-yellow-500 font-semibold")),n||(logger.info("Connection health restored"),e.textContent="Reconnected",setTimeout(()=>{"Reconnected"===e.textContent&&(healthMonitor.hasVersionMismatch?(e.textContent=`Version Mismatch (Panel: v4.3, Server: ${healthMonitor.serverVersion})`,e.className="text-red-500 font-semibold"):mediaStream?(e.textContent="Connected",e.className="text-primary font-semibold"):(e.textContent="Microphone Access Denied (Listen Only)",e.className="text-yellow-500 font-semibold"))},2e3)))}function initHealthMonitoring(){healthMonitor.checkInterval=setInterval(updateHealthStatus,2e3),logger.info("Health monitoring initialized")}async function refreshData(){try{const e=Date.now(),t=await authenticatedFetch("/radio/dispatch/status"),n=await t.json();n.version&&"v4.3"!==n.version?(healthMonitor.serverVersion=n.version,healthMonitor.hasVersionMismatch=!0,logger.warn(`Version mismatch detected - Panel: v4.3, Server: ${n.version}`)):n.version&&(healthMonitor.hasVersionMismatch=!1);const i=Date.now()-e;if(healthMonitor.lastServerResponse=Date.now(),i>2e3&&logger.warn(`Slow request detected: ${i}ms for dispatch status`),n.serverTimestamp){dispatchData.lastServerTimestamp=n.serverTimestamp;const e=n.serverTimestamp,t=Date.now(),i=Math.abs(t-e);i>5e3&&logger.debug(`Large time drift detected: ${i}ms between client and server`)}else logger.debug("Server response missing timestamp");let a=0;const o=new Set;n.channels&&Object.entries(n.channels).forEach(([e,t])=>{const n=t.speakers?.length||0,i=t.listeners?.length||0;a+=n+i,(n>0||i>0)&&o.add(e)}),n.activeAlerts&&Object.keys(n.activeAlerts).forEach(e=>{n.activeAlerts[e]&&o.add(e)}),n.panicStatus&&Object.keys(n.panicStatus).forEach(e=>{const t=n.panicStatus[e];t&&(Array.isArray(t)?t.length>0:Object.keys(t).length>0)&&o.add(e)}),document.getElementById("statusUserCount").textContent=a,document.getElementById("statusChannelCount").textContent=o.size,dispatchData.channels||(dispatchData.channels={});const s=n.channels||{},r=Object.keys(dispatchData.channels).length;Object.assign(dispatchData.channels,s),Object.keys(dispatchData.channels).forEach(e=>{s[e]||delete dispatchData.channels[e]}),dispatchData.users=n.users||{},dispatchData.panicStatus=n.panicStatus||{},dispatchData.activeAlerts=n.activeAlerts||{};const c=Object.keys(dispatchData.channels).length;c!==r&&logger.debug(`Channel count changed: ${r} -> ${c}`),updateZoneData(),logger.trace(`Data refreshed: ${a} users across ${o.size} channels`)}catch(e){logger.error(`Failed to refresh data: ${e.message||e}`),logger.debug(`Request failed at ${(new Date).toISOString()}, last successful response: ${new Date(healthMonitor.lastServerResponse).toISOString()}`),dispatchData.channels||(dispatchData.channels={}),dispatchData.users||(dispatchData.users={}),dispatchData.persistentAlerts||(dispatchData.persistentAlerts={}),dispatchData.panicStatus||(dispatchData.panicStatus={})}}function renderZoneLayout(){lastUserStates.clear(),cleanupDragAndDrop();const e=document.getElementById("zonesContainer");if(!dispatchData.config||!dispatchData.config.zones)return void(e.innerHTML='<div class="flex-1 flex items-center justify-center"><div class="bg-card border border-border text-card-foreground p-6 text-center rounded-lg shadow-lg">No zones configured</div></div>');logger.debug("Rendering zone layout with config:",Object.keys(dispatchData.config.zones));const t=Object.entries(dispatchData.config.zones).map(([e,t])=>{const n=t.Channels?Object.entries(t.Channels).map(([e,n])=>{const i="trunked"===n.type,a=normalizeFrequency(n.frequency);return`\n <div class="bg-card border border-border text-card-foreground p-4 mb-4 rounded-lg shadow-md hover:shadow-lg transition-all duration-200 channel-container" id="channel-container-${a}">\n <div class="channel-header cursor-pointer hover:bg-accent p-3 rounded-lg transition-colors mb-3" onclick="toggleChannel('${a}')">\n <div class="flex items-center justify-between">\n <div class="flex items-center gap-3 flex-1">\n <span class="channel-toggle text-muted-foreground">▼</span>\n <div class="font-medium text-foreground">${n.name}</div>\n <div id="channel-${a}-alert" class="hidden signal-indicator">\n <span class="uk-label text-white" id="channel-${a}-alert-text">ALERT</span>\n </div>\n <div id="channel-${a}-panic" class="hidden">\n <span class="uk-label bg-destructive text-destructive-foreground">PANIC</span>\n </div>\n </div>\n <div class="flex items-center gap-3">\n <div class="text-right">\n <div id="channel-${a}-count" class="text-sm font-medium text-foreground">0 users</div>\n <div id="channel-${a}-listeners" class="text-xs text-muted-foreground">0 listeners</div>\n </div>\n ${i?"":`\n <button id="scan-toggle-${a}" class="uk-btn scan-toggle-btn uk-btn-small" type="button" onclick="event.stopPropagation(); toggleChannelScan('${a}');" data-uk-tooltip="Toggle channel scan" style="display: none;">\n <div class="size-4">\n <uk-icon icon="headphones"></uk-icon>\n </div>\n </button>\n <div class="uk-inline" onclick="event.stopPropagation();">\n <button class="uk-btn bg-secondary text-secondary-foreground hover:bg-secondary/80 uk-btn-small hamburger-menu-btn" type="button" data-uk-tooltip="Channel actions">\n <div class="size-4">\n <uk-icon icon="more-vertical"></uk-icon>\n </div>\n </button>\n <div class="uk-drop uk-dropdown min-w-52 bg-popover border border-border shadow-xl rounded-lg" data-uk-dropdown="mode: click; pos: bottom-right; delay-hide: 0; delay-show: 0; auto-update: false; boundary: true; animate-out: false">\n <ul class="uk-nav uk-dropdown-nav p-1">\n ${generateAlertDropdownButtons(a,n.name)}\n </ul>\n </div>\n </div>\n `}\n </div>\n </div>\n <div class="channel-details mt-2 ml-6">\n <div class="text-sm text-muted-foreground">${normalizeFrequency(n.frequency)} MHz ${i?"(Trunked)":""}</div>\n <div class="text-xs text-muted-foreground">NAC: ${t.nacIds?t.nacIds.join(", "):"N/A"}</div>\n </div>\n </div>\n <div id="channel-${a}-users" class="channel-users space-y-2 min-h-[80px] bg-muted/50 p-4 rounded border border-border">\n <div class="text-xs text-muted-foreground italic flex items-center gap-2">\n <div class="size-4">\n <uk-icon icon="users"></uk-icon>\n </div>\n Loading...\n </div>\n </div>\n </div>\n `}).join(""):'<div class="text-muted-foreground text-sm p-4 bg-card rounded border border-border">No channels configured</div>',i=t.Channels?Object.values(t.Channels).reduce((e,t)=>{const n=normalizeFrequency(t.frequency).toString(),i=dispatchData.channels[n];return e+(i?.speakers?.length||0)},0):0;return`\n <div class="dispatch-zone p-4 zone-container flex-1 min-w-[350px] border-r border-border last:border-r-0 bg-muted/30" id="zone-container-${e}">\n <div class="mb-6 zone-header p-4 rounded-lg bg-card border border-border cursor-pointer hover:bg-accent transition-colors shadow-sm" onclick="toggleZone('${e}')">\n <div class="zone-expanded">\n <div class="flex items-center gap-3">\n <span class="zone-toggle text-muted-foreground text-lg">▼</span>\n <div class="flex-1">\n <div class="flex items-center gap-3 mb-2">\n <h2 class="text-lg font-semibold text-foreground">${t.name}</h2>\n <span class="uk-label bg-chart-1 text-foreground">${i} active</span>\n </div>\n <div class="text-sm text-muted-foreground">Zone ${e}</div>\n <div class="text-xs text-muted-foreground mt-1 flex items-center gap-1">\n <div class="size-3">\n <uk-icon icon="hash"></uk-icon>\n </div>\n NAC IDs: ${t.nacIds?t.nacIds.join(", "):"None"}\n </div>\n </div>\n </div>\n </div>\n <div class="zone-collapsed hidden">\n <div class="text-center">\n <div class="text-sm font-semibold text-foreground">${t.name}</div>\n <div class="text-xs text-muted-foreground mt-1">${i} active</div>\n </div>\n </div>\n </div>\n <div class="zone-content space-y-4">\n ${n}\n </div>\n </div>\n `}).join("");e.innerHTML=t,logger.debug("Zone layout rendered, initializing drag and drop"),requestAnimationFrame(()=>{initializeDragAndDrop(),(dispatchData.channels||dispatchData.persistentAlerts||dispatchData.panicStatus)&&updateZoneData()},150)}function updateZoneData(){if(!dispatchData.config||!dispatchData.config.zones)return void logger.warn("No config data available for updateZoneData");if(isDragging)return;dispatchData.channels||(dispatchData.channels={}),dispatchData.users||(dispatchData.users={}),dispatchData.activeAlerts||(dispatchData.activeAlerts={}),dispatchData.panicStatus||(dispatchData.panicStatus={}),logger.trace("Updating zone data with channels:",Object.keys(dispatchData.channels||{}));let e=!1;Object.entries(dispatchData.config.zones).forEach(([t,n])=>{if(!n.Channels)return;let i=!1;Object.entries(n.Channels).forEach(([t,n])=>{const a=normalizeFrequency(n.frequency).toString(),o="trunked"===n.type;let s={speakers:[],listeners:[],activeTalkers:[]};if(o&&n.frequencyRange){const[e,t]=n.frequencyRange;Object.entries(dispatchData.channels).forEach(([n,i])=>{const a=normalizeFrequency(n);a>=e&&a<=t&&(s.speakers.push(...i.speakers||[]),s.listeners.push(...i.listeners||[]),s.activeTalkers.push(...i.activeTalkers||[]))})}else{const e=dispatchData.channels[a];e&&(s=e)}const r=document.getElementById(`channel-${a}-count`),c=document.getElementById(`channel-${a}-listeners`),d=document.getElementById(`channel-${a}-users`),l=document.getElementById(`channel-${a}-alert`),u=document.getElementById(`channel-${a}-panic`);if(!r||!c||!d)return void logger.warn(`Missing UI elements for channel ${a}`);if(l){const e=document.getElementById(`channel-${a}-alert-text`),t=dispatchData.activeAlerts?.[a];t?(l.classList.remove("hidden"),e&&(e.textContent=t.name,t.color&&(e.style.backgroundColor=t.color,e.style.borderRadius="0.375rem",e.style.padding="0.25rem 0.5rem",e.style.transition="none",e.style.pointerEvents="none"))):l.classList.add("hidden")}if(dispatchData.config?.alerts&&Object.values(dispatchData.config.alerts).forEach((e,t)=>{if(e.isPersistent){const n=document.getElementById(`alert-toggle-${a}-${t}`);if(n){const t=dispatchData.activeAlerts?.[a]&&dispatchData.activeAlerts[a].name===e.name,i=t?`Clear ${e.name}`:`Trigger ${e.name}`,o=t?"shield-check":"alert-triangle";n.innerHTML=`\n <div class="size-4 mr-2" style="color: ${e.color||"#dc2626"}">\n <uk-icon icon="${o}"></uk-icon>\n </div>\n ${i}\n `,n.style.color=e.color||"#dc2626"}}}),u){const e=dispatchData.panicStatus?.[a];if(e&&(Array.isArray(e)?e.length>0:Object.keys(e).length>0)){u.classList.remove("hidden");const e=`${a}-panic`;notifiedPanics.has(e)||(notifiedPanics.add(e),UIkit.notification({message:`<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-3 flex items-center'><uk-icon icon='siren'></uk-icon></span> PANIC BUTTON activated on ${a} MHz!</div>`,status:"danger",timeout:0}))}else{u.classList.add("hidden");const e=`${a}-panic`;notifiedPanics.delete(e)}}const g=s.speakers||[],p=s.listeners||[],h=g.length,m=p.length;(h>0||m>0)&&(logger.trace(`Channel ${a}: ${h} speakers, ${m} listeners`),i=!0),r.textContent=`${h} user${1!==h?"s":""}`,c.textContent=`${m} listener${1!==m?"s":""}`;const f=document.getElementById("statusUserCount");if(f){const e=Object.values(dispatchData.channels).reduce((e,t)=>e+(t.speakers?.length||0),0);f.textContent=e}const y=document.getElementById("statusChannelCount");if(y){let e=0;Object.values(dispatchData.channels).forEach(t=>{const n=t.speakers?.length||0,i=t.listeners?.length||0;(n>0||i>0)&&e++}),Object.keys(dispatchData.activeAlerts||{}).forEach(t=>{if(dispatchData.activeAlerts[t]){dispatchData.channels[t]&&(dispatchData.channels[t].speakers?.length>0||dispatchData.channels[t].listeners?.length>0)||e++}}),Object.keys(dispatchData.panicStatus||{}).forEach(t=>{const n=dispatchData.panicStatus[t];if(n&&(Array.isArray(n)?n.length>0:Object.keys(n).length>0)){const n=dispatchData.channels[t]&&(dispatchData.channels[t].speakers?.length>0||dispatchData.channels[t].listeners?.length>0),i=dispatchData.persistentAlerts?.[t];n||i||e++}}),y.textContent=e}updateChannelUsersWithDiff(a,s.speakers||[],s,d),currentDispatchChannel===a&&(e=!0)});let a=0,o=!1;n.Channels&&Object.values(n.Channels).forEach(e=>{const n=normalizeFrequency(e.frequency),i=dispatchData.channels[n];if(logger.trace(`Zone ${t} - Channel ${e.frequency} normalized to ${n}, found data: ${JSON.stringify(i)}`),i){const e=i.speakers?.length||0;logger.debug(`Adding ${e} speakers from channel ${n}`),a+=e,e>0&&(o=!0)}const s=dispatchData.activeAlerts?.[n],r=dispatchData.panicStatus?.[n],c=r&&(Array.isArray(r)?r.length>0:Object.keys(r).length>0);(s||c)&&(o=!0,logger.debug(`Channel ${n} is active due to alert=${s} or panic=${c}`))}),logger.trace(`Zone ${t} final active count: ${a}`);const s=document.querySelector(`#zone-container-${t} .zone-expanded .uk-label`),r=document.querySelector(`#zone-container-${t} .zone-collapsed .text-xs`),c=o?a>0?`${a} active`:"active":"0 active";s&&(s.textContent=c,s.className=o?"uk-label bg-chart-1 text-white":"uk-label bg-muted text-muted-foreground"),r&&(r.textContent=c),(i||o)&&logger.trace(`Zone ${t} updated with ${a} active users, hasActiveChannels=${o}`)}),e&&refreshDispatchUserDisplay(),updateScanButtonStates(),updateScanButtonVisibility()}function updateScanButtonStates(){scannedChannels.forEach(e=>{const t=document.getElementById(`scan-toggle-${e}`);t&&(t.classList.add("bg-accent"),t.setAttribute("data-uk-tooltip","Stop scanning channel"))})}function updateScanButtonVisibility(){dispatchData.config&&dispatchData.config.zones&&Object.entries(dispatchData.config.zones).forEach(([e,t])=>{t.Channels&&Object.entries(t.Channels).forEach(([e,t])=>{const n=normalizeFrequency(t.frequency),i=document.getElementById(`scan-toggle-${n}`),a="trunked"===t.type;i&&!a&&(i.style.display=n===currentDispatchChannel?"none":"inline-block")})})}function _toggleZone(e){const t=document.getElementById(`zone-container-${e}`),n=t.querySelector(".zone-toggle"),i=t.querySelector(".zone-expanded"),a=t.querySelector(".zone-collapsed");t.classList.toggle("collapsed"),t.classList.contains("collapsed")?(n.textContent="▶",i.classList.add("hidden"),a.classList.remove("hidden")):(n.textContent="▼",i.classList.remove("hidden"),a.classList.add("hidden"))}function isBroadcastModalOpen(){const e=document.getElementById("broadcast-modal");return e?.classList.contains("uk-open")}function _toggleChannel(e){const t=document.getElementById(`channel-container-${e}`),n=t.querySelector(".channel-toggle");t.classList.toggle("collapsed"),n.textContent=t.classList.contains("collapsed")?"▶":"▼"}function _openBroadcastModal(e,t){logger.debug("openBroadcastModal called"),document.querySelectorAll("[data-uk-dropdown]").forEach(e=>{UIkit.dropdown(e).hide(!1)});const n=document.getElementById("tone-selector"),i=document.getElementById("selected-tone");if(n){try{UIkit.tab(n).show(0)}catch(e){n.querySelectorAll("li").forEach((e,t)=>{e.classList.toggle("uk-active",0===t)})}i&&(i.value="none",logger.debug("Reset tone selector to 'none'"))}document.getElementById("target-frequency").value=e,document.getElementById("target-channel-name").textContent=t,logger.debug(`Opening broadcast modal for frequency: ${e}, channelName: ${t}`),UIkit.modal("#broadcast-modal").show()}async function _sendBroadcastAlert(){const e=document.getElementById("target-frequency").value,t=document.getElementById("alert-type").value,n=document.getElementById("alert-message").value,i=document.getElementById("selected-tone").value;if(logger.debug(`Broadcast alert - selectedTone: "${i}", type: "${t}", message: "${n}"`),logger.debug("Available tones for comparison:",Object.keys(dispatchData.tones||{})),!n.trim())return void UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Please enter a message</div>",status:"danger",timeout:3e3});let a=null;dispatchData.config?.alerts&&(a=Object.values(dispatchData.config.alerts).find(e=>e.name===t));const o=normalizeFrequency(e);if(a?.isPersistent){const e=dispatchData.activeAlerts?.[o];if(e?.isPersistent&&e.name!==t)try{await authenticatedFetch("/radio/dispatch/alert/clear",{method:"POST",body:JSON.stringify({frequency:o})})}catch(e){logger.error(`Failed to clear existing persistent alert: ${e}`)}dispatchData.activeAlerts[o]=a?{...a}:{name:t,color:null,isPersistent:!0,tone:null}}else a||"General Alert"===t||logger.warn(`Unknown alert type: ${t}, treating as non-persistent`);const s=a?.tones?.activate||a?.tone||null;logger.info(`Sending broadcast: ${t} to ${o} MHz with tone: ${i||s||"none"}`);try{const r=i&&"none"!==i?i:s;await authenticatedFetch("/radio/dispatch/broadcast",{method:"POST",body:JSON.stringify({frequency:o,type:t,message:n,tone:r,isPersistent:!!a&&a.isPersistent,color:a?a.color:null})}),logger.info("Broadcast alert sent successfully"),r&&"none"!==r?isChannelMonitored(o)?dispatchData.tones?.[r.toUpperCase()]?(logger.debug(`Broadcast: Playing tone locally for dispatcher: ${r} (monitoring channel ${o})`),playDispatchTone(r)):logger.warn(`Broadcast: Cannot play tone ${r} - tones available:`,Object.keys(dispatchData.tones||{})):logger.debug(`Broadcast: Not playing tone ${r} locally - not monitoring channel ${o}`):logger.debug("No tone selected or configured"),UIkit.notification({message:`<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='megaphone'></uk-icon></span> Broadcast sent to ${e} MHz</div>`,status:"success",timeout:3e3}),UIkit.modal("#broadcast-modal").hide(),document.getElementById("alert-message").value="",document.getElementById("selected-tone").value="none",document.getElementById("alert-type").value="General Alert",document.getElementById("alert-type-text").textContent="General Alert"}catch(e){logger.error(`Failed to send broadcast: ${e}`),UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to send broadcast</div>",status:"danger",timeout:5e3})}}async function _toggleAlert(e,t,n=null){e=normalizeFrequency(e);let i=null;if(dispatchData.config?.alerts&&(i=Object.values(dispatchData.config.alerts).find(e=>e.name===t)),!i)return void logger.error(`Alert configuration not found for: ${t}`);if(!i.isPersistent)return void logger.error(`Alert ${t} is not persistent and cannot be toggled`);const a=dispatchData.activeAlerts[e]&&dispatchData.activeAlerts[e].name===t,o=i.tones?.activate||i.tone||null,s=i.tones?.deactivate||i.tone||null,r=a?s:o;r&&isChannelMonitored(e)?dispatchData.tones?.[r.toUpperCase()]?(logger.debug(`Toggle Alert: Playing ${a?"deactivate":"activate"} tone locally for dispatcher: ${r} (monitoring channel ${e})`),playDispatchTone(r)):logger.warn(`Toggle Alert: Cannot play tone ${r} - tones available:`,Object.keys(dispatchData.tones||{})):r&&logger.debug(`Toggle Alert: Not playing tone ${r} locally - not monitoring channel ${e}`);try{const n=a?"/radio/dispatch/alert/clear":"/radio/dispatch/alert/trigger",o=await authenticatedFetch(n,{method:"POST",body:JSON.stringify({frequency:e,alertType:t,alertConfig:i})});if(!o.ok)throw new Error(`Server responded with ${o.status}`);a?delete dispatchData.activeAlerts[e]:dispatchData.activeAlerts[e]=i,updateZoneData(),logger.info(`${t} ${a?"cleared":"triggered"} on ${e}`),UIkit.notification({message:`<div class='flex items-center bg-chart-3 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> ${t} ${a?"cleared":"activated"} on ${e} MHz</div>`,status:a?"success":"warning",timeout:5e3})}catch(e){logger.error(`Failed to toggle ${t}: ${e}`),UIkit.notification({message:`<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to toggle ${t}</div>`,status:"danger",timeout:3e3})}if(n?.target){const e=n.target.closest(".uk-inline");if(e){const t=UIkit.dropdown(e.querySelector("[data-uk-dropdown]"));t&&t.hide(!1)}}return!1}async function _triggerAlert(e,t,n=null){n&&n.preventDefault();let i=null;if(dispatchData.config?.alerts&&(i=Object.values(dispatchData.config.alerts).find(e=>e.name===t)),!i)return void logger.error(`Alert configuration not found for: ${t}`);if(i.isPersistent)return void logger.error(`Alert ${t} is persistent and should use toggleAlert instead`);const a=i.tones?.activate||i.tone||null,o=normalizeFrequency(e),s=(dispatchData.config?.alerts?Object.values(dispatchData.config.alerts).filter(e=>!e.isPersistent):[]).findIndex(e=>e.name===t),r=-1!==s?document.getElementById(`alert-trigger-${o}-${s}`):null;r&&(r.innerHTML=`\n <div class="size-4 mr-2" style="color: ${i.color||"#059669"}">\n <uk-icon icon="check"></uk-icon>\n </div>\n ${t} Sent\n `);try{const n=await authenticatedFetch("/radio/dispatch/alert/oneshot",{method:"POST",body:JSON.stringify({frequency:o,alertConfig:i})});if(!n.ok)throw new Error(`Server responded with ${n.status}`);logger.info(`${t} triggered on ${e}`),UIkit.notification({message:`<div class='flex items-center bg-chart-1 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='volume-2'></uk-icon></span> ${t} sent to ${e} MHz</div>`,status:"success",timeout:3e3}),a&&isChannelMonitored(e)?dispatchData.tones?.[a.toUpperCase()]?(logger.debug(`Trigger Alert: Playing tone locally for dispatcher: ${a} (monitoring channel ${e})`),playDispatchTone(a)):logger.warn(`Trigger Alert: Cannot play tone ${a} - tones available:`,Object.keys(dispatchData.tones||{})):a&&logger.debug(`Trigger Alert: Not playing tone ${a} locally - not monitoring channel ${e}`),r&&setTimeout(()=>{r.innerHTML=`\n <div class="size-4 mr-2" style="color: ${i.color||"#059669"}">\n <uk-icon icon="volume-2"></uk-icon>\n </div>\n ${t}\n `},2e3)}catch(e){logger.error(`Failed to trigger ${t}: ${e}`),UIkit.notification({message:`<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to send ${t}</div>`,status:"danger",timeout:3e3}),r&&(r.innerHTML=`\n <div class="size-4 mr-2" style="color: ${i.color||"#059669"}">\n <uk-icon icon="volume-2"></uk-icon>\n </div>\n ${t}\n `)}if(n?.target){const e=n.target.closest("[data-uk-dropdown]");e&&UIkit.dropdown(e).hide(!1)}}async function _playTone(e,t,n=null){const i=t.toUpperCase();isChannelMonitored(e)?(logger.debug(`Play Tone: Playing tone locally for dispatcher: ${i} (monitoring channel ${e})`),playDispatchTone(i)):logger.debug(`Play Tone: Not playing tone ${i} locally - not monitoring channel ${e}`);try{await authenticatedFetch("/api/play-tone",{method:"POST",body:JSON.stringify({frequency:e,tone:i})}),logger.info(`${i} tone played on ${e}`),UIkit.notification({message:`<div class='flex items-center bg-chart-1 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='volume-2'></uk-icon></span> ${i} tone sent to ${e} MHz</div>`,status:"success",timeout:2e3})}catch(e){logger.error(`Failed to play ${t} tone: ${e}`),UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to send tone</div>",status:"danger",timeout:3e3})}if(n?.target){const e=n.target.closest(".uk-inline");if(e){const t=UIkit.dropdown(e.querySelector("[data-uk-dropdown]"));t&&t.hide(!1)}}}let currentAlertData={};function _openUserAlertModal(e,t,n){document.querySelectorAll("[data-uk-dropdown]").forEach(e=>{UIkit.dropdown(e).hide(!1)}),currentAlertData={userId:parseInt(e,10),userName:t,frequency:n},document.getElementById("user-alert-target").textContent=`Send alert to ${t}`,document.getElementById("user-alert-message").value="",UIkit.modal("#user-alert-modal").show()}async function _sendUserAlert(){logger.debug("sendUserAlert called");const e=document.getElementById("user-alert-message");if(logger.debug("Message element:",e),!e)return void logger.error("Could not find alert-message element");const t=e.value.trim();if(logger.debug("Message value:",`"${t}"`),logger.debug("Message length:",t.length),!t)return logger.debug("Message is empty, showing notification"),void UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Please enter a message</div>",status:"danger",timeout:3e3});logger.info(`Sending alert to user ${currentAlertData.userName}: ${t}`);try{const e=await authenticatedFetch("/dispatch/user/alert",{method:"POST",body:JSON.stringify({userId:currentAlertData.userId,message:t,frequency:currentAlertData.frequency})}),n=await e.json();n.success?(logger.info(`Alert sent to user ${currentAlertData.userName}`),UIkit.notification({message:`<div class='flex items-center bg-chart-3 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='bell'></uk-icon></span> Alert sent to ${currentAlertData.userName}</div>`,status:"success",timeout:3e3}),UIkit.modal("#user-alert-modal").hide()):logger.error(`Failed to send alert to user ${currentAlertData.userName}:`,n.error||"Unknown error")}catch(e){logger.error(`Error sending alert to user ${currentAlertData.userName}:`,e)}}let currentCallsignData={};function _openChangeCallsignModal(e,t){document.querySelectorAll("[data-uk-dropdown]").forEach(e=>{UIkit.dropdown(e).hide(!1)}),currentCallsignData={userId:parseInt(e,10),userName:t},document.getElementById("change-callsign-target").textContent=`Set callsign for ${t} (ID: ${e})`;const n=document.getElementById("callsign-input");n.value=t||"",n.select(),UIkit.modal("#change-callsign-modal").show(),setTimeout(()=>n.focus(),100)}async function _confirmChangeCallsign(){const e=document.getElementById("callsign-input").value.trim();try{const t=await authenticatedFetch("/dispatch/user/set-player-callsign",{method:"POST",body:JSON.stringify({userId:currentCallsignData.userId,callsign:e})}),n=await t.json();t.ok?(logger.info(`Callsign for ${currentCallsignData.userName} (ID: ${currentCallsignData.userId}) updated to "${e||"(default)"}"`),UIkit.notification({message:`<div class='flex items-center bg-primary/20 text-primary p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='pen-line'></uk-icon></span> Callsign ${e?`set to ${e}`:"reset to default"} for ${currentCallsignData.userName}</div>`,status:"success",timeout:3e3}),UIkit.modal("#change-callsign-modal").hide()):(logger.error(`Failed to update callsign: ${n.error}`),UIkit.notification({message:`<div class='flex items-center bg-destructive/20 text-destructive p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to update callsign: ${n.error}</div>`,status:"error",timeout:5e3}))}catch(e){logger.error(`Error updating callsign for ${currentCallsignData.userName}:`,e),UIkit.notification({message:"<div class='flex items-center bg-destructive/20 text-destructive p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Error updating callsign</div>",status:"error",timeout:5e3})}}let currentDisconnectData={};function _openDisconnectModal(e,t){document.querySelectorAll("[data-uk-dropdown]").forEach(e=>{UIkit.dropdown(e).hide(!1)}),currentDisconnectData={userId:parseInt(e,10),userName:t},document.getElementById("disconnect-confirm-text").textContent=`Are you sure you want to disconnect ${t} from the radio system?`,UIkit.modal("#disconnect-confirm-modal").show()}async function _confirmDisconnectUser(){try{const e=await authenticatedFetch("/dispatch/user/disconnect",{method:"POST",body:JSON.stringify({userId:currentDisconnectData.userId})}),t=await e.json();t.success?(logger.info(`User ${currentDisconnectData.userName} disconnected successfully`),UIkit.notification({message:`<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='user-x'></uk-icon></span> ${currentDisconnectData.userName} disconnected</div>`,status:"warning",timeout:3e3}),UIkit.modal("#disconnect-confirm-modal").hide()):logger.error(`Failed to disconnect user ${currentDisconnectData.userName}: ${t.error}`)}catch(e){logger.error(`Error disconnecting user ${currentDisconnectData.userName}:`,e)}}function _selectAlertType(e){logger.debug(`selectAlertType called with: ${e}`),document.getElementById("alert-type").value=e,document.getElementById("alert-type-text").textContent=e,logger.debug(`Alert type set to: ${document.getElementById("alert-type").value}`),logger.debug(`Alert type text set to: ${document.getElementById("alert-type-text").textContent}`);const t=document.getElementById("alert-type-dropdown");t&&UIkit.dropdown(t).hide(),setTimeout(()=>{logger.debug(`After 100ms - Alert type is: ${document.getElementById("alert-type").value}`),logger.debug(`After 100ms - Alert type text is: ${document.getElementById("alert-type-text").textContent}`),logger.debug(`After 100ms - Alert message is: "${document.getElementById("alert-message").value}"`)},100),setTimeout(()=>{logger.debug(`After 600ms - Alert type is: ${document.getElementById("alert-type").value}`),logger.debug(`After 600ms - Alert type text is: ${document.getElementById("alert-type-text").textContent}`),logger.debug(`After 600ms - Alert message is: "${document.getElementById("alert-message").value}"`)},600)}function updateChannelUsersWithDiff(e,t,n,i){if(isDragging)return void logger.debug(`Skipping update for channel ${e} - dragging`);const a=`channel-${e}`;currentDispatchChannel===e&&dispatchUserId&&(t.includes(dispatchUserId)||t.includes(dispatchUserId.toString())||(t=[...t,dispatchUserId]));i.querySelectorAll("[data-user-id]").forEach(n=>{const i=n.getAttribute("data-user-id");if(!t.includes(i)&&!t.includes(parseInt(i,10))){const t=`${i}-${e}`;talkingTimeouts.has(t)&&(clearTimeout(talkingTimeouts.get(t)),talkingTimeouts.delete(t)),n.classList.remove("talking")}});const o=new Map;t.forEach(t=>{let i=dispatchData.users[t];t!==dispatchUserId&&t!==dispatchUserId?.toString()||(i={name:dispatchUserState.name,nacId:dispatchUserState.nacId});const a=n.activeTalkers||[],s=a.includes(parseInt(t,10))||a.includes(t.toString())||a.includes(t),r=dispatchData.panicStatus[e]||{},c=Array.isArray(r)?r.includes(t)||r.includes(parseInt(t,10))||r.map(e=>e.toString()).includes(t.toString()):Object.hasOwn(r,t)||Object.hasOwn(r,t.toString())||Object.hasOwn(r,parseInt(t,10))||Object.keys(r).includes(t.toString());o.set(t,{name:i&&i.name||`Player ${t}`,nacId:i&&i.nacId||"Unknown",isTalking:s,isInPanic:c})});const s=lastUserStates.get(a)||new Map;if(o.size!==s.size||Array.from(o.keys()).some(e=>!s.has(e))||Array.from(s.keys()).some(e=>!o.has(e))||0===o.size){let t="";if(o.size>0&&(t+=Array.from(o.entries()).map(([t,n])=>{const i=t===dispatchUserId||t?.toString()===dispatchUserId?.toString()||t<0||"DISPATCH"===t;return i&&logger.debug(`📊 Dispatch user data: userId=${t}, name="${n.name}", nacId="${n.nacId}"`),createUserHtml(t,n,e,i)}).join("")),""===t){let t="No active users",n="users";const a=dispatchData.channels&&Object.keys(dispatchData.channels).length>0||dispatchData.persistentAlerts&&Object.keys(dispatchData.persistentAlerts).length>0||dispatchData.panicStatus&&Object.keys(dispatchData.panicStatus).length>0,o=dispatchData.activeAlerts?.[e]||dispatchData.panicStatus?.[e];a||isAuthenticated?o?(t="Channel active (no users transmitting)",n="radio"):a||(t="Waiting for activity...",n="clock"):(t="Loading channel data...",n="loader-2");const s="loader-2"===n?"animate-spin":"";i.innerHTML=`<div class="text-xs text-muted-foreground italic p-3 flex items-center gap-2"><div class="size-4 ${s}"><uk-icon icon="${n}"></uk-icon></div>${t}</div>`}else i.innerHTML=t;requestAnimationFrame(()=>{sortableInstances.has(e)||initializeDragAndDrop(e)},200)}else o.forEach((t,n)=>{const i=s.get(n);i&&i.isTalking===t.isTalking&&i.isInPanic===t.isInPanic&&i.name===t.name&&i.nacId===t.nacId||updateUserElement(n,t,e)});lastUserStates.set(a,new Map(o))}function createUserHtml(e,t,n,i=!1){const a=i||"DISPATCH"===e||e<0;logger.debug(`createUserHtml: Checking isMyDispatch: userId=${e}, dispatchUserId=${dispatchUserId}, userState.name="${t.name}", dispatchUserState.name="${dispatchUserState.name}"`);const o=e===dispatchUserId||e?.toString()===dispatchUserId?.toString();logger.debug(`createUserHtml: Result: isMyDispatch=${o} (Fixed: Only using unique dispatchUserId for identification)`),a&&logger.info(`🚨 DISPATCH IDENTIFICATION: userId=${e}, dispatchUserId=${dispatchUserId}, userName="${t.name}", myName="${dispatchUserState.name}", isMyDispatch=${o} (Using unique ID only - no name/NAC fallback)`);const s=o?"This is your dispatch session":a?"Another dispatcher":"";return`\n <div class="${a?o?"dispatch-user user-item":"dispatch-user user-item drag-disabled other-dispatch":"user-item"} ${t.isTalking&&!t.isInPanic?"talking":""} flex items-center justify-between p-3 rounded-lg border ${t.isInPanic?"panic-user-flash bg-destructive/20 border-destructive drag-disabled":a?"":t.isTalking&&!t.isInPanic?"bg-chart-2/20 border-chart-2":"bg-card border-border"} transition-all duration-200"\n data-user-id="${e}"\n data-current-channel="${n}"\n data-user-name="${t.name}"\n data-is-my-dispatch="${o}"\n data-is-panic="${t.isInPanic}"\n ${s?`title="${s}" uk-tooltip`:""}>\n <div class="flex items-center gap-3">\n ${t.isTalking?'\n <div class="w-2 h-2 rounded-full bg-primary animate-pulse shadow-sm"></div>\n <div class="size-4 text-primary">\n <uk-icon icon="mic"></uk-icon>\n </div>\n ':'\n <div class="w-2 h-2 rounded-full bg-muted-foreground"></div>\n <div class="size-4 text-muted-foreground">\n <uk-icon icon="mic-off"></uk-icon>\n </div>\n '}\n <div>\n <div class="text-sm font-medium ${t.isInPanic?"text-destructive-foreground":"text-foreground"}">${t.name}${o?" (You)":""}</div>\n <div class="text-xs ${t.isInPanic?"text-destructive-foreground/80":"text-muted-foreground"}">${a?getDispatchDisplayId(e):`ID: ${e} | NAC: ${t.nacId}`}</div>\n </div>\n </div>\n <div class="flex items-center gap-2">\n <div class="text-xs ${t.isInPanic?"text-destructive-foreground font-medium":"text-muted-foreground"} flex items-center gap-1">\n ${t.isInPanic?'\n <div class="size-3 text-destructive">\n <uk-icon icon="alert-triangle"></uk-icon>\n </div>\n PANIC\n ':a?"Dispatch":"Speaker"}\n </div>\n ${a?"":`\n <div class="uk-inline" onclick="event.stopPropagation();">\n <button class="uk-btn bg-secondary text-secondary-foreground hover:bg-secondary/80 uk-btn-small px-2 py-1 hamburger-menu-btn" type="button" data-uk-tooltip="User actions">\n <div class="size-4">\n <uk-icon icon="more-vertical"></uk-icon>\n </div>\n </button>\n <div class="uk-drop uk-dropdown min-w-48 bg-popover border border-border shadow-xl rounded-lg" data-uk-dropdown="mode: click; pos: bottom-right; delay-hide: 0; delay-show: 0; auto-update: false; boundary: true; animate-out: false; flip: false">\n <ul class="uk-nav uk-dropdown-nav p-1">\n <li><a href="#" onclick="openUserAlertModal('${e}', '${t.name}', '${n}'); event.preventDefault();" class="text-chart-3 hover:bg-accent hover:text-chart-3 rounded px-3 py-2 flex items-center transition-colors">\n <div class="size-4 mr-2">\n <uk-icon icon="bell"></uk-icon>\n </div>\n Send Alert\n </a></li>\n ${dispatchData.config?.useCallsignSystem?`\n <li class="uk-nav-divider border-t border-border my-1"></li>\n <li><a href="#" onclick="openChangeCallsignModal('${e}', '${t.name}'); event.preventDefault();" class="text-primary hover:bg-accent hover:text-primary rounded px-3 py-2 flex items-center transition-colors">\n <div class="size-4 mr-2">\n <uk-icon icon="pen-line"></uk-icon>\n </div>\n Change Callsign\n </a></li>\n `:""}\n <li class="uk-nav-divider border-t border-border my-1"></li>\n <li><a href="#" onclick="openDisconnectModal('${e}', '${t.name}'); event.preventDefault();" class="text-destructive hover:bg-accent hover:text-destructive rounded px-3 py-2 flex items-center transition-colors">\n <div class="size-4 mr-2">\n <uk-icon icon="user-x"></uk-icon>\n </div>\n Disconnect User\n </a></li>\n </ul>\n </div>\n </div>\n `}\n </div>\n </div>\n `}function updateUserElement(e,t,n){const i=document.querySelector(`[data-user-id="${e}"][data-current-channel="${n}"]`);if(!i)return;const a="DISPATCH"===e||e<0||e===dispatchUserId||e===dispatchUserId?.toString();logger.debug(`Checking isMyDispatch: userId=${e}, dispatchUserId=${dispatchUserId}, userState.name="${t.name}", dispatchUserState.name="${dispatchUserState.name}"`);const o=e===dispatchUserId||e?.toString()===dispatchUserId?.toString();logger.debug(`Result: isMyDispatch=${o} (Fixed: Only using unique dispatchUserId)`),a&&logger.debug(`🔄 Updating dispatch user: userId=${e}, isMyDispatch=${o}, talking=${t.isTalking}, name="${t.name}" (Fixed identification logic)`);const s=a?o?"dispatch-user user-item":"dispatch-user user-item drag-disabled other-dispatch":"user-item",r=t.isTalking&&!t.isInPanic?"talking":"";i.className=`${s} ${r} flex items-center justify-between p-3 rounded-lg border ${t.isInPanic?"panic-user-flash bg-destructive/20 border-destructive drag-disabled":a?"":t.isTalking&&!t.isInPanic?"bg-chart-2/20 border-chart-2":"bg-card border-border"} transition-all duration-200`,i.dataset.isPanic=t.isInPanic,i.dataset.userName=t.name,i.dataset.isMyDispatch=o;const c=i.querySelectorAll(".w-2, .size-4");if(c.length>=2){const e=c[0],n=c[1];t.isTalking?(e.className="w-2 h-2 rounded-full bg-primary animate-pulse shadow-sm",n.className="size-4 text-primary",n.innerHTML='<uk-icon icon="mic"></uk-icon>'):(e.className="w-2 h-2 rounded-full bg-muted-foreground",n.className="size-4 text-muted-foreground",n.innerHTML='<uk-icon icon="mic-off"></uk-icon>')}const d=i.querySelector(".text-sm.font-medium"),l=i.querySelector(".text-xs");d&&(d.textContent=t.name,d.className="text-sm font-medium "+(t.isInPanic?"text-destructive-foreground":"text-foreground")),l&&(l.textContent.includes("NAC:")||l.textContent.includes("Session:")||l.textContent.includes("TNAC:"))&&(l.textContent=a?getDispatchDisplayId(e):`ID: ${e} | NAC: ${t.nacId}`,l.className="text-xs "+(t.isInPanic?"text-destructive-foreground/80":"text-muted-foreground"));const u=i.querySelector(".text-xs.flex.items-center.gap-1");u&&(u.className=`text-xs ${t.isInPanic?"text-destructive-foreground font-medium":"text-muted-foreground"} flex items-center gap-1`,u.innerHTML=t.isInPanic?'\n <div class="size-3 text-destructive">\n <uk-icon icon="alert-triangle"></uk-icon>\n </div>\n PANIC\n ':a?"Dispatch":"Speaker")}const sortableInstances=new Map;function cleanupDragAndDrop(){sortableInstances.forEach((e,t)=>{if(e)try{e.destroy()}catch(e){logger.warn(`Error destroying sortable instance: ${e}`)}}),sortableInstances.clear()}function initializeDragAndDrop(e){if(null==e){logger.debug("Initializing drag and drop for all channels");return void document.querySelectorAll('[id^="channel-"][id$="-users"]').forEach(e=>{const t=e.id.match(/^channel-(.+)-users$/);if(t){const e=t[1];"undefined"!==e&&"null"!==e&&initializeDragAndDrop(e)}})}if(sortableInstances.has(e))return void logger.debug(`Drag already initialized for channel: ${e}`);if(logger.debug(`Trying to init drag for channel: ${e}`),"undefined"==typeof Sortable)return void logger.error("SortableJS not loaded!");const t=document.getElementById(`channel-${e}-users`);if(!t)return void logger.warn(`Channel element not found: ${e}`);if(t.sortable)return void logger.warn(`Element already has sortable instance: ${e}`);const n=sortableInstances.get(e);if(n){try{n.destroy()}catch(e){logger.warn(`Error destroying instance: ${e}`)}sortableInstances.delete(e)}try{const n=new Sortable(t,{group:"radio-users",draggable:".user-item:not(.other-dispatch)",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",fallbackClass:"sortable-fallback",filter:".other-dispatch",preventOnFilter:!1,forceFallback:isDesktopApp,fallbackOnBody:!1,fallbackTolerance:0,animation:150,onChoose:e=>{if(e.item.classList.contains("other-dispatch"))return logger.warn("Preventing selection of other dispatch user"),!1},onStart:e=>{logger.debug("DRAG STARTED!");if(e.item.classList.contains("other-dispatch"))return logger.warn("Preventing drag of other dispatch user"),!1;isDragging=!0,document.querySelectorAll(".channel-users").forEach(t=>{t!==e.from&&t.classList.add("drag-over")}),document.body.classList.add("is-dragging")},onMove:e=>{const t=e.dragged,n=t.classList.contains("other-dispatch"),i="true"===t.getAttribute("data-is-panic");return!n&&!i||(logger.warn("Blocking move of other dispatch user or panicking user"),!1)},onEnd:e=>{logger.debug("DRAG ENDED!",{from:e.from?.id,to:e.to?.id,itemId:e.item?.dataset?.userId,oldIndex:e.oldIndex,newIndex:e.newIndex}),document.querySelectorAll(".channel-users").forEach(e=>{e.classList.remove("drag-over")}),document.body.classList.remove("is-dragging"),e.from!==e.to?handleUserChannelSwitch(e):requestAnimationFrame(()=>{isDragging=!1})}});sortableInstances.set(e,n),t.sortable=n,logger.debug(`Sortable created successfully for: ${e}`)}catch(e){logger.error(`Failed to create sortable: ${e}`),isDragging=!1}}async function handleUserChannelSwitch(e){const t=e.item,n=t.dataset.userId;if(t.classList.contains("other-dispatch"))return logger.warn("Attempted to move other dispatch user - preventing"),void(isDragging=!1);const i=t.dataset.currentChannel,a=t.dataset.userName,o=e.to.closest(".channel-container");if(!o)return logger.error("Could not find target channel container"),e.from.appendChild(e.item),void requestAnimationFrame(()=>{isDragging=!1});const s=o.id.replace("channel-container-","");if(n&&s&&i!==s){if("DISPATCH"===n||n<0){const e=currentDispatchChannel;return logger.info(`Moving dispatch from ${e} to ${s}`),isDragging=!0,setDispatchChannel(normalizeFrequency(s)),UIkit.notification({message:`<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='headphones'></uk-icon></span> Dispatch now monitoring ${s} MHz</div>`,status:"success",timeout:3e3}),e&&lastUserStates.delete(`channel-${e}`),lastUserStates.delete(`channel-${s}`),void setTimeout(()=>{isDragging=!1},200)}logger.info("Moving user "+n+" ("+a+") from channel "+i+" to "+s);try{const e=await authenticatedFetch("/radio/dispatch/switchChannel",{method:"POST",body:JSON.stringify({serverId:parseInt(n,10),frequency:normalizeFrequency(s),oldFrequency:normalizeFrequency(i)})}),o=await e.json();o.success?(UIkit.notification({message:`<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='check-circle'></uk-icon></span> ${a} moved to ${s} MHz</div>`,status:"success",timeout:3e3}),t.dataset.currentChannel=s,lastUserStates.delete(`channel-${i}`),lastUserStates.delete(`channel-${s}`),setTimeout(()=>{isDragging=!1},2e3)):(logger.error(`Failed to switch user ${n} to channel ${s}: ${o.error}`),UIkit.notification({message:`<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Failed to move ${a}: ${o.error}</div>`,status:"danger",timeout:5e3}),isDragging=!1)}catch(e){logger.error(`Error switching user channel: ${e.message}`),UIkit.notification({message:`<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Network error: Failed to move ${a}</div>`,status:"danger",timeout:5e3}),isDragging=!1}}else requestAnimationFrame(()=>{isDragging=!1})}setInterval(()=>{for(const[e,t]of sortableInstances.entries()){const n=document.getElementById(`channel-${e}-users`);if(!n){try{t.destroy()}catch(e){logger.warn(`Error destroying orphaned instance: ${e}`)}sortableInstances.delete(e),n&&(n.sortable=null)}}},3e4);let isDispatchConnected=!1,wasDispatchConnected=!1,dispatchReconnectionAttempts=0;const maxDispatchReconnectionAttempts=5;let dispatchReconnectionInterval=null,dispatchDisconnectionStartTime=null,dispatchUserNotificationTimeout=null,savedDispatchChannel=null;function saveDispatchState(){savedDispatchChannel=currentDispatchChannel,logger.info(`Saved dispatch state - channel: ${savedDispatchChannel}`)}function restoreDispatchState(){dispatchSocket&&dispatchSocket.connected?(logger.info(`Restoring dispatch state - channel: ${savedDispatchChannel}`),savedDispatchChannel&&setTimeout(()=>{setDispatchChannel(savedDispatchChannel),logger.info(`Restored dispatch channel: ${savedDispatchChannel}`)},500)):logger.warn("Cannot restore dispatch state - socket not connected")}async function ensureDispatchConnection(){if(!dispatchSocket||!dispatchSocket.connected){dispatchSocket&&(dispatchSocket.removeAllListeners(),dispatchSocket.disconnect(),dispatchSocket=null),logger.info("Creating new dispatch socket connection");try{const e=await authenticatedFetch("/radio/dispatch/config"),t=await e.json(),n=isDesktopApp&&desktopEndpoint?desktopEndpoint:`${window.location.protocol}//${window.location.host}`;return new Promise((e,i)=>{dispatchSocket=io(n,{auth:{authToken:t.authToken,serverId:dispatchUserId},reconnection:!1,timeout:5e3,forceNew:!0}),setupDispatchSocketEvents(),dispatchSocket.once("connect",()=>{logger.info("Dispatch socket connected successfully"),isDispatchConnected=!0,e(!0)}),dispatchSocket.once("connect_error",e=>{logger.error(`Dispatch socket connection error: ${e.message}`),isDispatchConnected=!1,i(e)}),setTimeout(()=>{isDispatchConnected||i(new Error("Dispatch connection timeout"))},5e3)})}catch(e){throw logger.error(`Failed to get config for socket connection: ${e.message}`),e}}return Promise.resolve(!0)}function attemptDispatchReconnection(){if(dispatchReconnectionAttempts>=5)return logger.error("Max dispatch reconnection attempts reached"),void showDispatchNotification("Dispatch connection could not be restored","error");dispatchReconnectionInterval||(dispatchReconnectionInterval=setInterval(async()=>{try{dispatchReconnectionAttempts++,logger.info(`Dispatch reconnection attempt ${dispatchReconnectionAttempts}/5`),await ensureDispatchConnection(),logger.info("Dispatch successfully reconnected!"),restoreDispatchState(),dispatchDisconnectionStartTime&&Date.now()-dispatchDisconnectionStartTime>5e3&&showDispatchNotification("Dispatch connection restored!","success"),updateVoiceStatus("listening"),dispatchReconnectionInterval&&(clearInterval(dispatchReconnectionInterval),dispatchReconnectionInterval=null)}catch(e){if(logger.error(`Dispatch reconnection attempt ${dispatchReconnectionAttempts} failed: ${e.message}`),dispatchReconnectionAttempts>=5)clearInterval(dispatchReconnectionInterval),dispatchReconnectionInterval=null,showDispatchNotification("Dispatch connection could not be restored","error");else{(dispatchDisconnectionStartTime?Date.now()-dispatchDisconnectionStartTime:0)>5e3&&showDispatchNotification(`Dispatch reconnecting... (${dispatchReconnectionAttempts}/5)`,"warning")}}},3e3))}function showDispatchNotification(e,t="info"){UIkit.notification({message:`<div class='flex items-center bg-${"success"===t?"chart-2":"error"===t?"destructive":"warning"===t?"chart-3":"chart-1"} text-${"error"===t?"destructive-foreground":"foreground"} p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='${{success:"check-circle",error:"alert-triangle",warning:"alert-triangle",info:"info"}[t]}'></uk-icon></span>${e}</div>`,status:{success:"success",error:"danger",warning:"warning",info:"primary"}[t],timeout:5e3})}async function initDispatchVoice(){try{logger.info("Initializing dispatch voice system...");const e=dispatchUserId,t=await authenticatedFetch("/radio/dispatch/config"),n=await t.json(),i=isDesktopApp&&desktopEndpoint?desktopEndpoint:`${window.location.protocol}//${window.location.host}`;dispatchSocket=io(i,{auth:{authToken:n.authToken,serverId:e},reconnection:!1,timeout:5e3}),dispatchUserId||(dispatchUserId=e),setupDispatchSocketEvents(),await setupAudioCapture(),setTimeout(()=>{setDefaultDispatchChannel()},500),logger.info(`Dispatch voice system initialized with ID: ${e}`)}catch(e){logger.error(`Failed to initialize dispatch voice system: ${e}`)}}function setDefaultDispatchChannel(){if(currentDispatchChannel)return void logger.debug(`Default dispatch channel already set: ${currentDispatchChannel}`);const e=document.querySelector('[id^="channel-"][id$="-users"]');if(!e)return logger.debug("Channel elements not ready, retrying in 100ms"),void setTimeout(()=>{setDefaultDispatchChannel()},100);const t=e.id.replace("channel-","").replace("-users","");logger.info(`Setting default dispatch channel to: ${t}`),setDispatchChannel(normalizeFrequency(t))}function scheduleZoneUpdate(){_zoneUpdatePending||(_zoneUpdatePending=!0,requestAnimationFrame(()=>{_zoneUpdatePending=!1,updateZoneData()}))}function setupDispatchSocketEvents(){dispatchSocket.on("connect",()=>{logger.info("Dispatch connected to voice system"),logger.debug(`Socket ID: ${dispatchSocket.id}`),logger.debug(`Socket connected: ${dispatchSocket.connected}`),isDispatchConnected=!0,wasDispatchConnected=!0,updateVoiceStatus("listening"),forceStopAllBackgroundAudio(),dispatchSessionId&&dispatchSocket.emit("setDispatchSession",dispatchSessionId);const e={name:dispatchUserState.name,callsign:dispatchUserState.name,nacId:dispatchUserState.nacId,serverId:dispatchUserId};logger.debug(`📤 Sending updateUserInfo on connect: ${JSON.stringify(e)}`),dispatchSocket.emit("updateUserInfo",e),scannedChannels.size>0&&(logger.debug(`📻 Re-registering ${scannedChannels.size} scanned channels on reconnect`),scannedChannels.forEach(e=>{dispatchSocket.emit("addListeningChannel",e),logger.debug(`📻 Re-sent addListeningChannel: ${e}`)})),dispatchReconnectionAttempts=0,dispatchDisconnectionStartTime=null,dispatchReconnectionInterval&&(clearInterval(dispatchReconnectionInterval),dispatchReconnectionInterval=null),dispatchUserNotificationTimeout&&(clearTimeout(dispatchUserNotificationTimeout),dispatchUserNotificationTimeout=null),wasDispatchConnected&&isAuthenticated?(logger.info("Reconnected after resource restart — re-authenticating session silently"),reauthenticateSession().then(e=>{e?(restoreDispatchState(),updateVoiceStatus("listening")):(logger.warn("Silent re-auth failed after reconnect; API actions may not work"),showDispatchNotification("Session could not be refreshed after reconnect. Please reload the page if actions fail.","warning"),restoreDispatchState(),updateVoiceStatus("listening"))})):wasDispatchConnected||logger.debug("First connection established, no state to restore")}),dispatchSocket.on("connect_error",e=>{logger.error(`Dispatch socket connection error: ${e}`),isDispatchConnected=!1,forceStopAllBackgroundAudio(),wasDispatchConnected&&!dispatchReconnectionInterval&&(logger.info("Dispatch connection error, starting reconnection"),updateVoiceStatus("reconnecting"),attemptDispatchReconnection())}),dispatchSocket.on("disconnect",e=>{logger.warn(`Dispatch disconnected from voice system, reason: ${e}`),isDispatchConnected=!1,updateVoiceStatus("disconnected"),destroyAllP25Decoders(),forceStopAllBackgroundAudio(),saveDispatchState(),dispatchDisconnectionStartTime||(dispatchDisconnectionStartTime=Date.now()),"client namespace disconnect"!==e&&wasDispatchConnected&&(logger.info("Unexpected dispatch disconnection, starting reconnection..."),dispatchUserNotificationTimeout=setTimeout(()=>{isDispatchConnected||showDispatchNotification("Dispatch connection lost. Attempting to reconnect...","warning")},5e3),dispatchReconnectionInterval||(updateVoiceStatus("reconnecting"),attemptDispatchReconnection()))}),dispatchSocket.on("voice",e=>{const t=normalizeFrequency(e.frequency);logger.debug(`🎙️ Received voice data: serverId=${e.serverId}, frequency=${e.frequency}, normalized=${t}, currentChannel=${currentDispatchChannel}, dataLength=${e.data?e.data.length:"null"}`),logger.debug(`🎙️ Scanned channels: [${Array.from(scannedChannels).join(", ")}], isScanned=${scannedChannels.has(t)}`),currentDispatchChannel&&t===currentDispatchChannel&&e.serverId!==dispatchUserId?(logger.debug(`🎙️ Playing main channel audio from user ${e.serverId} on channel ${t}`),playReceivedAudioWithRadioEffects(e)):!scannedChannels.has(t)||e.serverId===dispatchUserId||isPTTActive||isTransmitting||t===currentDispatchChannel||scanAudioMuted||!(null===currentlyTransmittingUser||currentlyTransmittingUser===e.serverId&&scannedChannels.has(normalizeFrequency(e.frequency)))?logger.debug(`🎙️ Ignoring voice data: channel mismatch, own dispatch, or blocked by priority (their: ${t}, ours: ${currentDispatchChannel}, isScanned: ${scannedChannels.has(t)}, isPTT: ${isPTTActive})`):(logger.debug(`🎙️ Playing scanned channel audio from user ${e.serverId} on channel ${t} (currentlyTransmittingUser: ${currentlyTransmittingUser})`),playReceivedAudioWithRadioEffects(e))}),dispatchSocket.on("talkingState",e=>{logger.debug(`Talking state update: ${JSON.stringify(e)}`);const t=e.serverId===dispatchUserId||e.serverId?.toString()===dispatchUserId?.toString();logger.debug(`Talking state: serverId=${e.serverId}, dispatchUserId=${dispatchUserId}, isOwnDispatch=${t}, state=${e.state}, frequency=${e.frequency}`);const n=normalizeFrequency(e.frequency),i=currentDispatchChannel&&n===currentDispatchChannel,a=scannedChannels.has(n);if(logger.debug(`🎙️ TalkingState: freq=${e.frequency}, normalized=${n}, isMainChannel=${i}, isScannedChannel=${a}`),i&&!t)if(e.state){if(logger.debug(`🔊 Playing TX_START tone for other user: ${e.serverId} - BLOCKING scan audio`),playDispatchTone("TX_START"),logger.debug("🔊 🔇 MUTING scan audio - main channel receive priority"),scanAudioMuted=!0,isTransmissionPlaying&¤tlyTransmittingUser&¤tlyTransmittingUser!==e.serverId&&(logger.debug("🔊 Stopping scan transmission sounds for main channel priority"),stopTransmissionSounds()),transmissionAudio.mid){logger.debug("🔊 Stopping existing transmission mid sound before starting new transmission");try{transmissionAudio.mid.pause(),transmissionAudio.mid.currentTime=0,transmissionAudio.mid=null}catch(e){logger.error(`❌ Error stopping existing transmission mid: ${e}`)}}logger.debug(`🔊 Checking analog transmission effects config: ${dispatchData.config?dispatchData.config.analogTransmissionEffects:"no config"}`),logger.debug("🔊 Full config object:",dispatchData.config),logger.debug("🔊 analogTransmissionEffects value:",dispatchData.config?.analogTransmissionEffects),dispatchData.config?.analogTransmissionEffects?(logger.debug("🔊 Starting transmission sounds"),startTransmissionSounds()):(logger.debug("🔊 NOT starting transmission sounds - config check failed"),logger.debug("🔊 Analog transmission effects disabled or no config")),currentlyTransmittingUser=e.serverId,checkAndStartBackgroundSounds(e.serverId)}else logger.debug(`🔊 Playing TX_END tone for other user: ${e.serverId} - RESTORING scan audio`),playDispatchTone("TX_END"),dispatchData.config?.analogTransmissionEffects?(logger.debug("🔊 Stopping transmission sounds for scanned channel"),stopTransmissionSounds()):logger.debug("🔊 Analog transmission effects disabled or no config"),currentlyTransmittingUser===e.serverId&&(currentlyTransmittingUser=null),stopBackgroundSoundsForUser(e.serverId),releaseP25Decoder(e.serverId),isPTTActive||isTransmitting?logger.debug(`🔊 Keeping scan audio muted - we are still transmitting (isPTTActive=${isPTTActive}, isTransmitting=${isTransmitting})`):(logger.debug("🔊 🔊 UNMUTING scan audio - main channel clear and we're not transmitting"),scanAudioMuted=!1);else if(!a||t||i){if(t)logger.debug(`🚫 Skipping TX tone for own dispatch session: ${e.serverId} (prevents double PTT_END)`);else if(transmissionAudio.mid){logger.debug("🔊 [CHANNEL CHANGE] Stopping transmission mid sound - not on monitored channel");try{transmissionAudio.mid.pause(),transmissionAudio.mid.currentTime=0,transmissionAudio.mid=null}catch(e){logger.error(`❌ Error stopping transmission mid on channel change: ${e}`)}}}else e.state?isPTTActive||isTransmitting||currentlyTransmittingUser?logger.debug(`🔊 BLOCKED scan transmission from ${e.serverId} - main channel has priority (isPTTActive=${isPTTActive}, isTransmitting=${isTransmitting}, currentlyTransmittingUser=${currentlyTransmittingUser})`):(logger.debug(`🔊 Starting scan transmission from user ${e.serverId} on scanned channel ${n}`),playDispatchTone("TX_START"),dispatchData.config?.analogTransmissionEffects&&(logger.debug("🔊 Starting transmission sounds for scanned channel"),startTransmissionSounds()),currentlyTransmittingUser=e.serverId,checkAndStartBackgroundSounds(e.serverId),updateChannelScanIndicator(n,!0)):(logger.debug(`🔊 Playing TX_END tone for scanned user: ${e.serverId}`),playDispatchTone("TX_END"),dispatchData.config?.analogTransmissionEffects&&(logger.debug("🔊 Stopping transmission sounds for scanned channel"),stopTransmissionSounds()),currentlyTransmittingUser===e.serverId&&(currentlyTransmittingUser=null),stopBackgroundSoundsForUser(e.serverId),releaseP25Decoder(e.serverId),updateChannelScanIndicator(n,!1));if(updateTalkingIndicator(e.serverId,e.frequency,e.state),!e.state&&transmissionAudio.mid){logger.debug("🔊 [FAILSAFE] Stopping transmission mid sound for ended transmission");try{transmissionAudio.mid&&(transmissionAudio.mid.pause(),transmissionAudio.mid.currentTime=0,transmissionAudio.mid=null)}catch(e){logger.error(`❌ Error in failsafe transmission stop: ${e}`)}}}),dispatchSocket.on("dispatchNotification",e=>{if(logger.info(`Received dispatch notification: ${e.type} on ${e.frequency} MHz`),!e.frequency||"alert_cleared"===e.type||isChannelMonitored(e.frequency)){if("alert_activated"===e.type)UIkit.notification({message:`<div class='flex items-center bg-chart-3 text-foreground p-3 rounded-lg'><span class='flex-none mr-3 flex items-center'><uk-icon icon='alert-triangle'></uk-icon></span> ${e.message}</div>`,status:"warning",timeout:0}),e.tone&&e.frequency&&isChannelMonitored(e.frequency)?dispatchData.tones?.[e.tone.toUpperCase()]?(logger.debug(`Socket Alert: Playing tone locally for dispatcher: ${e.tone} (monitoring channel ${e.frequency})`),playDispatchTone(e.tone)):logger.warn(`Socket Alert: Cannot play tone ${e.tone} - tones available:`,Object.keys(dispatchData.tones||{})):e.tone&&e.frequency?logger.debug(`Socket Alert: Not playing tone ${e.tone} locally - not monitoring channel ${e.frequency}`):e.tone&&logger.warn(`Socket Alert: Cannot play tone ${e.tone} - no frequency data provided`);else if("alert_cleared"===e.type)e.frequency&&isChannelMonitored(e.frequency)&&UIkit.notification({message:`<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-3 flex items-center'><uk-icon icon='check-circle'></uk-icon></span> ${e.message}</div>`,status:"success",timeout:5e3});else if("oneshot_alert"===e.type||"broadcast_alert"===e.type||"tone_triggered"===e.type){let t="",n="primary";const i=3e3;"oneshot_alert"===e.type?(t=`<div class='flex items-center bg-chart-3 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> ${e.message}</div>`,n="warning"):"broadcast_alert"===e.type?(t=`<div class='flex items-center bg-chart-1 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='megaphone'></uk-icon></span> ${e.message}</div>`,n="primary"):"tone_triggered"===e.type&&(t=`<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='volume-2'></uk-icon></span> ${e.message}</div>`,n="success"),UIkit.notification({message:t,status:n,timeout:i}),e.tone&&e.frequency&&isChannelMonitored(e.frequency)?dispatchData.tones?.[e.tone.toUpperCase()]?(logger.debug(`Socket ${e.type}: Playing tone locally for dispatcher: ${e.tone} (monitoring channel ${e.frequency})`),playDispatchTone(e.tone)):logger.warn(`Socket ${e.type}: Cannot play tone ${e.tone} - tones available:`,Object.keys(dispatchData.tones||{})):e.tone&&e.frequency?logger.debug(`Socket ${e.type}: Not playing tone ${e.tone} locally - not monitoring channel ${e.frequency}`):e.tone&&logger.warn(`Socket ${e.type}: Cannot play tone ${e.tone} - no frequency data provided`)}}else logger.debug(`Skipping notification for unmonitored channel ${e.frequency}: ${e.type}`)}),dispatchSocket.on("serverTone",e=>{if(!e||!e.tone)return;const t=e.frequency?Math.round(1e4*parseFloat(e.frequency))/1e4:null;if(!(null!==t&&(t===currentDispatchChannel||scannedChannels.has(t))))return void logger.debug(`serverTone: ignoring tone '${e.tone}' on ${t} — not our channel`);const n=e.tone.toUpperCase(),i=_selfPlayedTones.get(n);if(void 0!==i&&Date.now()-i<_SELF_PLAY_DEDUP_WINDOW_MS)return logger.debug(`serverTone: suppressing echo of self-played tone '${n}' (${Date.now()-i}ms ago)`),void _selfPlayedTones.delete(n);_selfPlayedTones.forEach((e,t)=>{Date.now()-e>=_SELF_PLAY_DEDUP_WINDOW_MS&&_selfPlayedTones.delete(t)}),logger.debug(`serverTone: playing tone '${n}' on ${t}`),playDispatchTone(n)}),dispatchSocket.on("updateSirenList",e=>{if(logger.debug("🚨 Received siren list update:",e),logger.debug("🚨 Data type:",typeof e,"Array:",Array.isArray(e)),logger.debug("🚨 Data keys:",Object.keys(e||{})),logger.debug("🚨 Data length:",e?.length),sirenPlayers.clear(),e&&"object"==typeof e)if(Array.isArray(e))logger.debug("🚨 Processing siren list as array format"),e.forEach((e,t)=>{if(logger.debug(`🚨 Array index ${t}: ${e} (type: ${typeof e})`),e){const e=t+1;sirenPlayers.set(e,!0),logger.debug(`🚨 Player ${e} has siren ON (from array index ${t})`)}});else{logger.debug("🚨 Processing siren list as object format");for(const[t,n]of Object.entries(e))logger.debug(`🚨 Object key ${t}: ${n} (type: ${typeof n})`),n&&(sirenPlayers.set(parseInt(t,10),!0),logger.debug(`🚨 Player ${t} has siren ON`))}logger.debug(`🚨 Final siren players: ${sirenPlayers.size} active - ${Array.from(sirenPlayers.keys())}`)}),dispatchSocket.on("updateHeliList",e=>{if(logger.debug("🚁 Received heli list update:",e),logger.debug("🚁 Data type:",typeof e,"Array:",Array.isArray(e)),logger.debug("🚁 Data keys:",Object.keys(e||{})),logger.debug("🚁 Data length:",e?.length),heliPlayers.clear(),e&&"object"==typeof e)if(Array.isArray(e))logger.debug("🚁 Processing heli list as array format"),e.forEach((e,t)=>{if(logger.debug(`🚁 Array index ${t}: ${e} (type: ${typeof e})`),e){const e=t+1;heliPlayers.set(e,!0),logger.debug(`🚁 Player ${e} has helicopter ON (from array index ${t})`)}});else{logger.debug("🚁 Processing heli list as object format");for(const[t,n]of Object.entries(e))logger.debug(`🚁 Object key ${t}: ${n} (type: ${typeof n})`),n&&(heliPlayers.set(parseInt(t,10),!0),logger.debug(`🚁 Player ${t} has helicopter ON`))}logger.debug(`🚁 Final heli players: ${heliPlayers.size} active - ${Array.from(heliPlayers.keys())}`)}),dispatchSocket.on("gunshotDuringTransmission",e=>{if(logger.info("🔫 Received gunshot during transmission:",e),e?.serverId){let t=!1;currentDispatchChannel&¤tlyTransmittingUser===e.serverId&&(t=!0,logger.info(`🔫 Gunshot from user ${e.serverId} on connected channel ${currentDispatchChannel}`)),!t&&dispatchData&&dispatchData.channels&&Object.entries(dispatchData.channels).forEach(([n,i])=>{i?.speakers&&i.speakers.forEach(i=>{if(i.serverId===e.serverId){const i=n;(i===currentDispatchChannel||scannedChannels.has(i))&&(t=!0,logger.info(`🔫 Gunshot from user ${e.serverId} on monitored channel ${i}`))}})}),t?(logger.info(`🔫 Playing individual gunshot for player ${e.serverId} at distance ${e.distance} - from monitored channel`),playIndividualGunshot(e.serverId,e.distance),recentGunshots.set(e.serverId,Date.now()),setTimeout(()=>{recentGunshots.delete(e.serverId)},3e3)):logger.info(`🔫 Ignoring gunshot from player ${e.serverId} - not from monitored channel`)}else logger.warn("🔫 Invalid gunshot data received:",e)}),dispatchSocket.on("channelState",e=>{if(!e||void 0===e.frequency||null===e.frequency)return;const t=normalizeFrequency(e.frequency).toString();e.empty?dispatchData.channels[t]&&(delete dispatchData.channels[t],logger.debug(`channelState: removed empty channel ${t}`),scheduleZoneUpdate()):(dispatchData.channels[t]={frequency:e.frequency,speakers:(e.speakers||[]).map(Number),listeners:(e.listeners||[]).map(Number),activeTalkers:(e.activeTalkers||[]).map(Number),empty:!1},logger.debug(`channelState: ${t} → ${(e.speakers||[]).length} speakers, ${(e.listeners||[]).length} listeners`),scheduleZoneUpdate())}),dispatchSocket.on("speakerJoined",e=>{if(!e||void 0===e.frequency)return;const t=normalizeFrequency(e.frequency).toString();dispatchData.channels[t]||(dispatchData.channels[t]={frequency:e.frequency,speakers:[],listeners:[],activeTalkers:[],empty:!1});const n=dispatchData.channels[t],i=Number(e.serverId);n.speakers.includes(i)||(n.speakers.push(i),n.empty=!1,logger.debug(`speakerJoined: ${i} joined ${t}`),scheduleZoneUpdate())}),dispatchSocket.on("speakerLeft",e=>{if(!e||void 0===e.frequency)return;const t=normalizeFrequency(e.frequency).toString(),n=dispatchData.channels[t];if(!n)return;const i=Number(e.serverId),a=n.speakers.length;n.speakers=n.speakers.filter(e=>e!==i),n.activeTalkers=(n.activeTalkers||[]).filter(e=>e!==i),n.speakers.length!==a&&(0===n.speakers.length&&0===n.listeners.length?delete dispatchData.channels[t]:n.empty=!1,logger.debug(`speakerLeft: ${i} left ${t}`),scheduleZoneUpdate())}),dispatchSocket.on("listenerJoined",e=>{if(!e||void 0===e.frequency)return;const t=normalizeFrequency(e.frequency).toString();dispatchData.channels[t]||(dispatchData.channels[t]={frequency:e.frequency,speakers:[],listeners:[],activeTalkers:[],empty:!1});const n=dispatchData.channels[t],i=Number(e.serverId);n.listeners.includes(i)||(n.listeners.push(i),n.empty=!1,logger.debug(`listenerJoined: ${i} joined ${t} as listener`),scheduleZoneUpdate())}),dispatchSocket.on("listenerLeft",e=>{if(!e||void 0===e.frequency)return;const t=normalizeFrequency(e.frequency).toString(),n=dispatchData.channels[t];if(!n)return;const i=Number(e.serverId),a=n.listeners.length;n.listeners=n.listeners.filter(e=>e!==i),n.listeners.length!==a&&(0===n.speakers.length&&0===n.listeners.length?delete dispatchData.channels[t]:n.empty=!1,logger.debug(`listenerLeft: ${i} left ${t} as listener`),scheduleZoneUpdate())})}async function setupAudioCapture(){teardownMicNormalizationChain();try{if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia)throw new Error("Media Devices API not available (requires HTTPS or localhost)");mediaStream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!1,channelCount:1}});try{if(audioContext=new(window.AudioContext||window.webkitAudioContext)({latencyHint:"interactive"}),"suspended"===audioContext.state)try{await Promise.race([audioContext.resume(),new Promise((e,t)=>setTimeout(()=>t(new Error("AudioContext resume timeout")),1e3))])}catch(e){logger.warn("AudioContext resume failed or timed out: "+e.message),logger.info("AudioContext will be resumed on first user interaction")}logger.info("Audio context initialized successfully")}catch(e){logger.error("Failed to create audio context:",e),audioContext=null}micNormalizationChain=buildMicNormalizationChain(audioContext,mediaStream),setupMediaRecorder(),logger.info("Audio capture initialized with optimized settings"),logger.debug(`Audio context state: ${audioContext?audioContext.state:"null"}`)}catch(e){logger.warn(`Failed to access microphone: ${e.message}`),logger.info("Continuing without microphone - user can still listen to radio");try{if(audioContext=new(window.AudioContext||window.webkitAudioContext)({latencyHint:"interactive"}),"suspended"===audioContext.state)try{await Promise.race([audioContext.resume(),new Promise((e,t)=>setTimeout(()=>t(new Error("AudioContext resume timeout")),1e3))])}catch(e){logger.warn("AudioContext resume failed or timed out: "+e.message),logger.info("AudioContext will be resumed on first user interaction")}logger.info("Audio context initialized successfully for listening only")}catch(e){logger.error("Failed to create audio context:",e),audioContext=null}teardownMicNormalizationChain(),mediaStream=null,mediaRecorder=null,UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='alert-triangle'></uk-icon></span> Microphone access denied - you can still listen to radio</div>",status:"danger",timeout:8e3})}logger.info(`Audio setup completed - microphone: ${mediaStream?"available":"unavailable"}, audio context: ${audioContext?"ready":"failed"}`)}function buildMicNormalizationChain(e,t){try{const n=e.createMediaStreamSource(t),i=e.createBiquadFilter();i.type="highpass",i.frequency.value=80,i.Q.value=.7;const a=e.createDynamicsCompressor();a.threshold.value=-18,a.knee.value=12,a.ratio.value=6,a.attack.value=.02,a.release.value=.4;const o=e.createDynamicsCompressor();o.threshold.value=-3,o.knee.value=0,o.ratio.value=20,o.attack.value=.002,o.release.value=.08;const s=e.createGain();s.gain.value=1;const r=e.createMediaStreamDestination();return n.connect(i),i.connect(a),a.connect(o),o.connect(s),s.connect(r),logger.info("🎙️ Send-side mic normalization chain built successfully"),{processedStream:r.stream,source:n,highpass:i,compressor:a,limiter:o,makeupGain:s,destination:r}}catch(e){return logger.error("🎙️ Failed to build mic normalization chain:",e),null}}function teardownMicNormalizationChain(){if(micNormalizationChain){try{micNormalizationChain.source.disconnect(),micNormalizationChain.highpass.disconnect(),micNormalizationChain.compressor.disconnect(),micNormalizationChain.limiter.disconnect(),micNormalizationChain.makeupGain.disconnect(),micNormalizationChain.destination.disconnect()}catch(e){}micNormalizationChain=null,logger.debug("🎙️ Mic normalization chain torn down")}}function setupMediaRecorder(){if(logger.debug(`🎙️ setupMediaRecorder called - mediaStream exists: ${!!mediaStream}`),mediaStream){logger.debug(`🎙️ mediaStream active: ${mediaStream.active}, tracks: ${mediaStream.getTracks().length}`);try{const e=micNormalizationChain?.processedStream??mediaStream;logger.debug("🎙️ Recording stream: "+(micNormalizationChain?"normalized (processed)":"raw (fallback)")),mediaRecorder=new MediaRecorder(e,{mimeType:"audio/webm; codecs=opus",audioBitsPerSecond:32e3});let t=[];mediaRecorder.ondataavailable=e=>{e.data.size>0&&t.push(e.data)},mediaRecorder.onstop=async()=>{if(t.length>0){const e=t.slice();t=[],isTransmitting&&recordCycle();if(!0===dispatchData.config?.radioFx?.p25Enabled){if(!_imbeRawModule)try{await loadDispatchImbeVocoder()}catch(e){logger.error("[DispatchIMBE Enc] WASM load failed, falling back to Opus:",e)}if(_imbeRawModule){let t=null;try{const n=getOrCreateDispatchEncoder();t=await n.encodeChunks(e)}catch(e){logger.warn("[DispatchIMBE Enc] encodeChunks failed, dropping packet:",e)}return void(t&¤tDispatchChannel&&(logger.debug("[DispatchIMBE Enc] Sending IMBE voice: channel="+currentDispatchChannel+", dataLength="+t.length),dispatchSocket.emit("voice",{channelName:currentDispatchChannel,serverId:dispatchUserId,data:t,encoding:"imbe"})))}}const n=await processAudioChunks(e);if(!n||0===n.trim().length)return;currentDispatchChannel&&(logger.debug("Sending voice data: channel="+currentDispatchChannel+", serverId="+dispatchUserId+", dataLength="+n.length),dispatchSocket.emit("voice",{channelName:currentDispatchChannel,serverId:dispatchUserId,data:n}))}else t=[]},mediaRecorder.onstart=()=>{t=[]},logger.info(`✅ MediaRecorder setup complete - state: ${mediaRecorder.state}`)}catch(e){logger.error(`❌ Failed to setup MediaRecorder: ${e}`),logger.error(`❌ MediaRecorder error stack: ${e.stack}`)}}else logger.error("❌ setupMediaRecorder: mediaStream is null")}async function processAudioChunks(e){if(!e||0===e.length)return logger.debug("processAudioChunks: No audio chunks to process"),"";const t=e.reduce((e,t)=>e+(t.size||0),0);if(0===t)return logger.debug("processAudioChunks: All chunks are empty (total size: 0)"),"";const n=new Blob(e,{type:"audio/webm; codecs=opus"});return 0===n.size?(logger.debug("processAudioChunks: Created blob is empty"),""):(logger.debug(`processAudioChunks: Processing ${e.length} chunks (${t} bytes total, blob size: ${n.size})`),await blobToBase64(n))}async function blobToBase64(e){return new Promise((t,n)=>{const i=new FileReader;i.onloadend=()=>{const e=";base64,",n=i.result.indexOf(e),a=-1!==n?i.result.slice(n+8):i.result.split(",")[1];t(a)},i.onerror=n,i.readAsDataURL(e)})}function startRecording(){if(logger.debug("startRecording called - mediaRecorder: "+!!mediaRecorder+" state: "+(mediaRecorder?mediaRecorder.state:"no recorder")),!mediaRecorder)return logger.error("❌ MediaRecorder is null - attempting to reinitialize"),void setupMediaRecorder();"inactive"===mediaRecorder.state?isTransmitting?logger.error("❌ Already transmitting"):mediaRecorder&&"inactive"===mediaRecorder.state&&!isTransmitting?(logger.debug("✅ Starting recording cycle"),isTransmitting=!0,recordCycle()):logger.debug("Cannot start recording - mediaRecorder missing, not inactive, or already transmitting"):logger.error(`❌ MediaRecorder in wrong state: ${mediaRecorder.state} (expected: inactive)`)}function stopRecording(){logger.debug("stopRecording called - setting isTransmitting=false");if(!0===dispatchData.config?.radioFx?.p25Enabled&&_imbeRawModule&&_dispatchImbeEncoder&¤tDispatchChannel){const e=_dispatchImbeEncoder.flush();e&&(logger.debug("[DispatchIMBE Enc] Sending flush packet: dataLength="+e.length),dispatchSocket.emit("voice",{channelName:currentDispatchChannel,serverId:dispatchUserId,data:e,encoding:"imbe"})),_dispatchImbeEncoder.reset(),logger.debug("[DispatchIMBE Enc] Encoder reset after PTT stop")}isTransmitting=!1,mediaRecorder&&"recording"===mediaRecorder.state?(logger.debug("Stopping active MediaRecorder"),mediaRecorder.stop()):logger.debug("MediaRecorder not recording - state: "+(mediaRecorder?mediaRecorder.state:"no recorder"))}function recordCycle(){isTransmitting&&"inactive"===mediaRecorder.state?(mediaRecorder.start(),setTimeout(()=>{"recording"===mediaRecorder.state&&mediaRecorder.stop()},300)):isTransmitting||logger.debug("recordCycle called but not transmitting - stopping")}let touchPTTActive=!1;function handlePTTTouchStart(e){e.preventDefault(),e.stopPropagation(),document.body.classList.add("ptt-active-mobile"),e.touches&&e.touches.length>1||(touchPTTActive=!0,navigator.vibrate&&navigator.vibrate(50),startPTT(),logger.debug("PTT touch start - mobile"))}function handlePTTTouchEnd(e){e.preventDefault(),e.stopPropagation(),document.body.classList.remove("ptt-active-mobile"),touchPTTActive&&(touchPTTActive=!1,navigator.vibrate&&navigator.vibrate(25),stopPTT(),logger.debug("PTT touch end - mobile"))}function _resetDispatchEncoderIfNeeded(){!0===dispatchData.config?.radioFx?.p25Enabled&&_imbeRawModule&&_dispatchImbeEncoder&&(_dispatchImbeEncoder._heldOutputs?.length>0||_dispatchImbeEncoder._remainder?.length>0)&&(_dispatchImbeEncoder.reset(),logger.debug("[DispatchIMBE Enc] Stale encoder state cleared at PTT start (aborted previous PTT)"))}function startPTT(){if(logger.debug(`startPTT called - currentDispatchChannel: ${currentDispatchChannel}, isPTTActive: ${isPTTActive}, mediaStream: ${!!mediaStream}`),currentDispatchChannel&&!isPTTActive)if(mediaStream)if(touchPTTActive&&event?.type?.startsWith("mouse"))logger.debug("Ignoring mouse event - touch is active");else{if(pttReleaseTimer)return clearTimeout(pttReleaseTimer),pttReleaseTimer=null,void logger.debug("Cancelled pending PTT release - user re-keyed PTT, staying keyed");logger.debug("🔊 🚨 PRIORITY: Main channel PTT - MUTING scan audio and stopping scan transmission sounds"),scanAudioMuted=!0,isTransmissionPlaying&¤tlyTransmittingUser&¤tlyTransmittingUser!==dispatchUserId&&(logger.debug(`🔊 🚨 PRIORITY: Stopping scan transmission sounds from user ${currentlyTransmittingUser}`),stopTransmissionSounds()),activeBgAudio.forEach((e,t)=>{const[n]=t.split("_"),i=parseInt(n,10);i!==dispatchUserId&&stopBackgroundSoundsForUser(i)}),document.getElementById("pttButton").classList.add("active","transmitting"),updateVoiceStatus("transmitting"),dispatchUserState.isTalking=!0,isPTTActive=!0,logger.debug(`Starting PTT on channel: ${currentDispatchChannel}`),document.getElementById("pttButton").classList.add("active","transmitting"),updateVoiceStatus("transmitting"),logger.debug(`Starting PTT - dispatch already speaker on: ${currentDispatchChannel}`),dispatchSocket.emit("setTalking",!0),dispatchUserState.isTalking=!0,currentDispatchChannel&&lastUserStates.delete(`channel-${currentDispatchChannel}`),playDispatchTone("PTT"),startRecording()}else UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-2'><uk-icon icon='mic-off'></uk-icon></span> Microphone access denied - cannot transmit</div>",status:"danger",timeout:5e3})}function getToneConfig(e){if(!dispatchData.tones||!e)return logger.warn("No tones data available or invalid tone name:",e),null;const t=e.toUpperCase(),n=dispatchData.tones[t];return null==n?(logger.warn(`Tone "${e}" not found in API tones data. Available tones:`,Object.keys(dispatchData.tones)),null):n}async function playWavTone(e){const t=buildAssetUrl(`audio/${e.toLowerCase()}.wav`);logger.debug(`Playing WAV tone "${e}" from: ${t}`),toneAudioContext||(toneAudioContext=new(window.AudioContext||window.webkitAudioContext)),"suspended"===toneAudioContext.state&&await toneAudioContext.resume();try{const n=await fetch(t);if(!n.ok)return void logger.warn(`WAV tone "${e}" not found at ${t} (${n.status})`);const i=await n.arrayBuffer(),a=await toneAudioContext.decodeAudioData(i);return new Promise(e=>{const t=toneAudioContext.createBufferSource();t.buffer=a;const n=toneAudioContext.createGain();n.gain.value=toneVolume,t.connect(n),n.connect(toneAudioContext.destination),t.onended=()=>e(),t.start()})}catch(t){logger.error(`WAV tone "${e}" playback failed: ${t}`)}}async function playToneSequence(e,t=0){logger.debug("Playing tone sequence:",e),toneAudioContext||(toneAudioContext=new(window.AudioContext||window.webkitAudioContext),logger.debug("Created new tone audio context")),"suspended"===toneAudioContext.state&&(logger.debug("Resuming suspended audio context"),await toneAudioContext.resume()),t>0&&await new Promise(e=>setTimeout(e,t));const n=toneAudioContext.currentTime;e.forEach((t,i)=>{let a=0;for(let t=0;t<i;t++)a+=e[t].delay||0;const o=toneAudioContext.createOscillator(),s=toneAudioContext.createGain();o.type="sine",o.frequency.value=t.freq;const r=toneVolume;logger.debug(`Playing tone ${t.freq}Hz for ${t.duration}ms with volume: ${r}`),s.gain.setValueAtTime(0,n+a/1e3),s.gain.linearRampToValueAtTime(r,n+(a+1)/1e3),s.gain.setValueAtTime(r,n+(a+t.duration-1)/1e3),s.gain.linearRampToValueAtTime(0,n+(a+t.duration)/1e3),o.connect(s),s.connect(toneAudioContext.destination),o.start(n+a/1e3),o.stop(n+(a+t.duration)/1e3)});const i=e.reduce((t,n,i)=>i===e.length-1?t+(n.delay||0)+n.duration:t+(n.delay||0),0);return new Promise(e=>setTimeout(e,i))}const _selfPlayedTones=new Map,_SELF_PLAY_DEDUP_WINDOW_MS=500;async function playDispatchTone(e){logger.debug(`Attempting to play tone: ${e}`);const t=getToneConfig(e);if(null!=t)return _selfPlayedTones.set(e.toUpperCase(),Date.now()),Array.isArray(t)&&0===t.length?(logger.debug(`Playing WAV tone: ${e}`),playWavTone(e)):(logger.debug(`Playing tone: ${e} with config:`,t),playToneSequence(t,0));logger.warn(`Tone "${e}" not found`)}function stopPTT(){isPTTActive&&(logger.debug(`Stopping PTT on channel: ${currentDispatchChannel}`),playDispatchTone("PTT_END"),logger.debug("🔊 Played immediate PTT_END tone (910Hz) for UI feedback"),document.getElementById("pttButton").classList.remove("active","transmitting"),updateVoiceStatus("listening"),pttReleaseTimer=setTimeout(()=>{pttReleaseTimer?(pttReleaseTimer=null,isPTTActive=!1,isTransmitting=!1,logger.debug("Dispatch stopped talking but remains speaker on: "+currentDispatchChannel),dispatchUserState.isTalking=!1,currentDispatchChannel&&lastUserStates.delete(`channel-${currentDispatchChannel}`),stopRecording(),dispatchSocket.emit("setTalking",!1),logger.debug("🔊 Main channel PTT ended - UNMUTING scan audio"),scanAudioMuted=!1,setTimeout(()=>{isPTTActive||isTransmitting||!currentlyTransmittingUser||currentlyTransmittingUser===dispatchUserId||(logger.debug(`🔊 Restoring scan transmission sounds for user ${currentlyTransmittingUser}`),dispatchData.config?.analogTransmissionEffects&&startTransmissionSounds(),checkAndStartBackgroundSounds(currentlyTransmittingUser))},100),setTimeout(()=>{validateAudioState()},2e3)):logger.debug("PTT release timer was cancelled - user re-keyed before delay expired")},350))}let persistentRadioFX=null;function computeDispatchRadioFxParams(){const e=dispatchData.config?.radioFx||{};if(!(!1!==e.fxEnabled))return{enabled:!1};function t(e,t,n){return Math.max(t,Math.min(n,e))}const n=t(e.highpassFrequency??250,80,8e3),i=t(e.lowpassFrequency??3400,1200,8e3),a=t(e.inputGain??1.2,.5,4),o=t(e.midBoost??2,-12,12),s=t(e.compression??60,0,100)/100;return{enabled:!0,inputGain:a,highpassFrequency:n,highpassQ:.7,lowpassFrequency:i,lowpassQ:.7,softener1Freq:5e3,softener2Gain:-2,preCompressorThreshold:-32*s,preCompressorRatio:1+13*s,compressorThreshold:-27*s,compressorRatio:1+19*s,limiterThreshold:-4*s,limiterRatio:1+19*s,distortionAmount:t(e.distortion??20,0,100)/100,midBoost:o}}function makeDispatchTubeSaturationCurve(e){null==e&&(e=1);const t=44100,n=new Float32Array(t);for(let i=0;i<t;i++){const a=2*i/t-1,o=a;let s;s=Math.abs(a)<.3?a*(1+.35*a*a):Math.sign(a)*(.3+.4*Math.tanh(5*(Math.abs(a)-.3))),n[i]=o*(1-e)+s*e}return n}function getOrCreateRadioFXChain(){const e=!1!==dispatchData.config?.radioFx?.fxEnabled;if(persistentRadioFX&&persistentRadioFX.ctx===audioContext&&persistentRadioFX.fxEnabled===e)return persistentRadioFX.outputGain.gain.value=.5*(window.dispatchVoiceVolume||1),persistentRadioFX;const t=computeDispatchRadioFxParams();logger.info("🔊 Creating persistent radio FX chain (one-time), enabled="+t.enabled);const n=audioContext.createGain(),i=audioContext.createGain();if(i.gain.value=.5*(window.dispatchVoiceVolume||1),!t.enabled)return n.gain.value=1,n.connect(i),i.connect(audioContext.destination),logger.info("🔊 Radio FX bypassed (disabled in config)"),persistentRadioFX={ctx:audioContext,fxEnabled:!1,inputGain:n,outputGain:i},persistentRadioFX;const a=audioContext.createDynamicsCompressor(),o=audioContext.createDynamicsCompressor(),s=audioContext.createDynamicsCompressor(),r=audioContext.createBiquadFilter(),c=audioContext.createBiquadFilter(),d=audioContext.createWaveShaper(),l=audioContext.createBiquadFilter(),u=audioContext.createBiquadFilter(),g=audioContext.createBiquadFilter();return a.threshold.value=t.preCompressorThreshold,a.knee.value=0,a.ratio.value=t.preCompressorRatio,a.attack.value=5e-4,a.release.value=.03,o.threshold.value=t.compressorThreshold,o.knee.value=0,o.ratio.value=t.compressorRatio,o.attack.value=5e-4,o.release.value=.03,s.threshold.value=t.limiterThreshold,s.knee.value=0,s.ratio.value=t.limiterRatio,s.attack.value=5e-4,s.release.value=.005,r.type="highpass",r.frequency.value=t.highpassFrequency,r.Q.value=t.highpassQ,c.type="lowpass",c.frequency.value=t.lowpassFrequency,c.Q.value=t.lowpassQ,d.curve=makeDispatchTubeSaturationCurve(t.distortionAmount),d.oversample="2x",u.type="lowpass",u.frequency.value=t.softener1Freq,u.Q.value=.7,g.type="highshelf",g.frequency.value=2800,g.Q.value=.7,g.gain.value=t.softener2Gain,l.type="peaking",l.frequency.value=1200,l.Q.value=1.2,l.gain.value=t.midBoost,n.gain.value=t.inputGain,n.connect(r),r.connect(c),c.connect(u),u.connect(g),g.connect(a),a.connect(o),o.connect(s),s.connect(d),d.connect(l),l.connect(i),i.connect(audioContext.destination),logger.info("🔊 Radio FX applied (highpass="+t.highpassFrequency+"Hz, lowpass="+t.lowpassFrequency+"Hz, compression="+(dispatchData.config?.radioFx?.compression??60)+")"),persistentRadioFX={ctx:audioContext,fxEnabled:!0,inputGain:n,outputGain:i},persistentRadioFX}function playReceivedAudioWithRadioEffects(e,t=!1){if(!audioContext)return void logger.debug("No audio context available for received audio");if("suspended"===audioContext.state)return logger.debug("Audio context suspended, attempting to resume"),void audioContext.resume().then(()=>{logger.debug("Audio context resumed, retrying audio playback"),playReceivedAudioWithRadioEffects(e)}).catch(e=>{logger.error("Failed to resume audio context:",e)});if(!e||!e.data)return void logger.debug("No voice data to play");const n=!0===dispatchData.config?.radioFx?.p25Enabled;if("imbe"===e.encoding&&n)return logger.debug(`[DispatchIMBE] Enqueuing IMBE packet from serverId=${e.serverId}, dataLen=${e.data.length}`),void enqueueImbePacket(e.serverId,e.data).catch(e=>{logger.error("[DispatchIMBE] enqueueImbePacket error:",e)});try{if("string"!=typeof e.data||0===e.data.length)return void logger.debug("Invalid voice data format");let t;try{t=atob(e.data)}catch(e){return void logger.error("Failed to decode base64 audio data:",e)}const n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e);const i=n.buffer;if(0===i.byteLength)return void logger.debug("Empty audio buffer, skipping playback");audioContext.decodeAudioData(i.slice()).then(t=>{if(!t||0===t.length)return void logger.debug("Audio decode returned null/empty buffer");const n=getOrCreateRadioFXChain(),i=audioContext.createBufferSource();i.buffer=t,i.connect(n.inputGain);try{i.start(),logger.debug(`Successfully playing audio from user ${e.serverId}`)}catch(e){logger.error("Error starting audio source:",e)}}).catch(t=>{logger.error("Audio decode error:",t),logger.debug(`Failed to decode audio data from user ${e.serverId}`)})}catch(e){logger.error("Error in audio processing pipeline:",e),logger.debug("Audio error details:",e.stack)}}const activeBgAudio=new Map;let currentlyTransmittingUser=null,transmissionAudio={start:null,mid:null,end:null},isTransmissionPlaying=!1;function checkAndStartBackgroundSounds(e){if(!e)return;const t=parseInt(e,10);if(Number.isNaN(t))return;if(!dispatchData.config||!dispatchData.config.playTransmissionEffects)return void logger.debug("🔊 Skipping background sounds - playTransmissionEffects is disabled");const n=sirenPlayers.has(t),i=heliPlayers.has(t);logger.debug(`🔊 Checking background sounds for player ${t}: siren=${n}, heli=${i}`),n&&!activeBgAudio.has(`${t}_siren`)&&handleBackgroundSound(t,"siren",!0,buildAssetUrl("audio/bgSiren.wav"),!0),i&&!activeBgAudio.has(`${t}_heli`)&&handleBackgroundSound(t,"heli",!0,buildAssetUrl("audio/bgHeli.wav"),!0)}function handleBackgroundSound(e,t,n,i,a){const o=`${e}_${t}`,s=activeBgAudio.has(o);if(logger.debug(`🔊 handleBackgroundSound: serverId=${e}, type=${t}, shouldPlay=${n}, isPlaying=${s}`),n&&!s){if(activeBgAudio.has(o))return void logger.debug(`🔊 ${t} sound already exists for player ${e}, skipping`);logger.debug(`🔊 Starting ${t} background sound for player ${e} from ${i}`),logger.debug(`🔊 Audio context state: ${audioContext?audioContext.state:"null"}`),playBackgroundSoundWithRadioEffects(e,t,i,a)}else if(!n&&s){const n=activeBgAudio.get(o);if(n)try{n.source&&n.source.stop(),n.animationId&&cancelAnimationFrame(n.animationId),activeBgAudio.delete(o),logger.debug(`⏹️ Stopped ${t} background sound for player ${e}`)}catch(e){logger.debug(`Error stopping ${t} audio: ${e}`),activeBgAudio.delete(o)}}else s&&logger.debug(`🔊 ${t} background sound already playing for player ${e}`)}function playBackgroundSoundWithRadioEffects(e,t,n,i){const a=`${e}_${t}`;if(!audioContext)return void logger.error("No audio context available for background sound");if("suspended"===audioContext.state)return logger.debug(`🔊 Audio context suspended, resuming for ${t} background sound`),void audioContext.resume().then(()=>{logger.debug(`🔊 Audio context resumed, starting ${t} background sound`),playBackgroundSoundWithRadioEffects(e,t,n,i)}).catch(e=>{logger.error(`❌ Failed to resume audio context: ${e}`)});logger.debug(`🔊 Attempting to load ${t} background audio from: ${n}`);let o=n;if(isDesktopApp&&desktopEndpoint&&!n.startsWith("http")){const e=n.startsWith("/")?n.substring(1):n;o=`${desktopEndpoint.replace(/\/$/,"")}/${e}`,logger.debug(`🔊 Desktop app audio URL: ${o}`)}fetch(o).then(e=>{if(logger.debug(`🔊 Fetch response for ${t}: status=${e.status}, ok=${e.ok}`),!e.ok)throw new Error(`HTTP ${e.status}: ${e.statusText}`);return e.arrayBuffer()}).then(e=>(logger.debug(`🔊 ArrayBuffer loaded for ${t}: size=${e.byteLength} bytes`),audioContext.decodeAudioData(e))).then(n=>{logger.debug(`🔊 Audio decoded for ${t}: duration=${n.duration}s, channels=${n.numberOfChannels}`);const o=audioContext.createBufferSource();o.buffer=n,o.loop=i;const s=audioContext.createGain(),r=audioContext.createDynamicsCompressor(),c=audioContext.createBiquadFilter(),d=audioContext.createBiquadFilter(),l=audioContext.createGain();r.threshold.value=-25,r.knee.value=5,r.ratio.value=3,r.attack.value=.005,r.release.value=.2,c.type="highpass",c.frequency.value=150,c.Q.value=.5,d.type="lowpass","siren"===t?(d.frequency.value=2400,d.Q.value=2):"heli"===t&&(d.frequency.value=1800,d.Q.value=4),s.gain.value=.6,l.gain.value=1*(window.dispatchSfxVolume||1),o.connect(s),s.connect(r),r.connect(c),c.connect(d),d.connect(l),l.connect(audioContext.destination),o.start(),logger.debug(`🔊 Audio source started for ${t} background sound`);const u=Date.now();let g=0;const p=Math.random()*Math.PI*2,h=Math.random()*Math.PI*2,m=Math.random()*Math.PI*2,f=()=>{g++;const e=(Date.now()-u)/1e3,n=.4*(.2*Math.sin(1.5*e*Math.PI+p)+.3*Math.sin(.6*e*Math.PI+h)+.25*Math.sin(.25*e*Math.PI+m)+.1*(Math.random()-.5))+.6,i=Math.max(.2,n-.15);let o;o=.35;const s=i*o*(window.dispatchSfxVolume||1);l.gain.value=s,g%60==0&&logger.debug(`🔊 Volume update: ${t} vol=${s.toFixed(3)}, bias=${i.toFixed(3)}, mult=${o.toFixed(3)}`),activeBgAudio.has(a)&&requestAnimationFrame(f)},y=requestAnimationFrame(f);activeBgAudio.set(a,{source:o,animationId:y,outputGain:l}),i||o.addEventListener("ended",()=>{y&&cancelAnimationFrame(y),activeBgAudio.delete(a),logger.debug(`🔊 ${t} sound ended for player ${e}`)}),logger.debug(`✅ Successfully started ${t} background sound with radio effects for player ${e}`)}).catch(e=>{logger.error(`❌ Failed to load/play ${t} sound: ${e}`),logger.error(`❌ Error details: ${e.name} - ${e.message}`),logger.error(`❌ Audio source: ${n}`),logger.debug(`🔄 Attempting fallback to simple Audio for ${t}`);const o=new Audio(n);o.loop=i,o.volume=.15,o.play().then(()=>{activeBgAudio.set(a,{source:o,animationId:null}),logger.debug(`✅ Fallback audio started for ${t}`)}).catch(e=>{logger.error(`❌ Fallback audio also failed: ${e}`)})})}function stopAllBackgroundSounds(){logger.debug("🔊 Legacy stopAllBackgroundSounds called - redirecting to comprehensive cleanup"),forceStopAllBackgroundAudio()}function stopBackgroundSoundsForUser(e){if(!e)return;const t=parseInt(e,10);if(Number.isNaN(t))return void logger.debug(`🔊 Invalid serverId for stop: ${e}`);logger.debug(`🔊 Stopping background sounds for player ${t}`);const n=[];activeBgAudio.forEach((e,i)=>{if(i.startsWith(`${t}_`)){logger.debug(`⏹️ Stopping ${i}`);try{e.source&&e.source.stop(),e.animationId&&cancelAnimationFrame(e.animationId)}catch(e){logger.debug(`Error stopping audio ${i}: ${e}`)}n.push(i)}}),n.forEach(e=>{activeBgAudio.delete(e)}),forceStopTransmissionSounds(),logger.debug(`⏹️ Stopped ${n.length} background sounds for user ${t}`)}function playIndividualGunshot(e,t){const n=parseInt(e,10);if(Number.isNaN(n))return void logger.debug(`🔫 Invalid serverId for gunshot: ${e}`);if(!dispatchData.config||!dispatchData.config.playTransmissionEffects)return void logger.debug("🔫 Skipping gunshot - playTransmissionEffects is disabled");const i=buildAssetUrl("audio/bgShot.wav"),a=new Audio(i);let o=.35;null!=t&&(o=Math.max(.09,Math.min(.35,.35*(1-t/150)))),a.volume=o,a.addEventListener("error",e=>{logger.error(`❌ Gunshot audio loading error (${i}): ${e.message||"Unknown error"}`)}),a.play().then(()=>{logger.info(`🔫 Successfully played individual gunshot for player ${n} at distance ${t} with volume ${o}`)}).catch(e=>{logger.error(`❌ Failed to play gunshot sound: ${e}`),logger.error(`❌ Audio source: ${i}`)})}function _playReceivedAudio(e){playReceivedAudioWithRadioEffects(e)}function updateVoiceStatus(e){const t=document.getElementById("voiceStatusText");t.textContent={muted:"MUTED",listening:"LISTENING",transmitting:"TRANSMITTING",disconnected:"DISCONNECTED",reconnecting:"RECONNECTING"}[e]||"UNKNOWN",t.className=`voice-status ${e}`}function updateTalkingIndicator(e,t,n){if(logger.debug(`updateTalkingIndicator: serverId=${e}, frequency=${t}, isTalking=${n}, dispatchUserId=${dispatchUserId}`),e===dispatchUserId){logger.debug(`Updating dispatch user talking indicator: ${n}`);const t=document.querySelectorAll(`.dispatch-user[data-user-id="${e}"]`);return logger.debug(`Found ${t.length} dispatch elements`),void t.forEach(e=>{n?(e.classList.add("talking"),logger.debug("Added talking class to dispatch element")):(e.classList.remove("talking"),logger.debug("Removed talking class from dispatch element"))})}const i=document.querySelector(`[data-user-id="${e}"][data-current-channel="${t}"]`);if(logger.debug(`Found user element for ${e}: ${!!i}`),i){const a=`${e}-${t}`;talkingTimeouts.has(a)&&(clearTimeout(talkingTimeouts.get(a)),talkingTimeouts.delete(a),logger.debug(`Cleared existing timeout for ${a}`)),n?(i.classList.add("talking"),logger.debug(`Added talking class to user ${e}`)):(i.classList.remove("talking"),logger.debug(`Removed talking class from user ${e}`))}else logger.warn(`Could not find user element for serverId=${e}, frequency=${t}`)}function refreshDispatchUserDisplay(){logger.debug("Refreshing dispatch user display for channel: "+currentDispatchChannel),lastUserStates.clear(),updateZoneData()}function setDispatchChannel(e){logger.info(`Setting dispatch channel to: ${e}`),stopAllBackgroundSounds(),forceStopAllBackgroundAudio();const t=normalizeFrequency(e);if(scannedChannels.has(t)){scannedChannels.delete(t),channelsAutoUnscanned.add(t),dispatchSocket?.connected&&(dispatchSocket.emit("removeListeningChannel",t),logger.debug(`📻 Removed ${t} from server listening channels (now main channel)`));const n=document.getElementById(`scan-toggle-${t}`);n&&(n.classList.remove("bg-accent"),n.setAttribute("data-uk-tooltip","Toggle channel scan")),logger.debug(`🔊 Auto-unscanned new main channel ${e} to prevent double audio`)}setTimeout(()=>{updateScanButtonVisibility()},100);const n=currentDispatchChannel;if(currentDispatchChannel){const e=normalizeFrequency(currentDispatchChannel);if(dispatchSocket.emit("setSpeakerChannel",null),scannedChannels.has(e)&&(dispatchSocket.emit("addListeningChannel",e),logger.debug(`📻 Re-added ${e} as listening channel (was main, now scanned)`)),channelsAutoUnscanned.has(e)&&e!==t){channelsAutoUnscanned.delete(e),scannedChannels.add(e),dispatchSocket?.connected&&dispatchSocket.emit("addListeningChannel",e);const t=document.getElementById(`scan-toggle-${e}`);t&&(t.classList.add("bg-accent"),t.setAttribute("data-uk-tooltip","Stop scanning channel")),logger.debug(`📻 Restored scan for ${e} after leaving it as main channel`)}lastUserStates.delete(`channel-${currentDispatchChannel}`)}if(currentDispatchChannel=normalizeFrequency(e),savedDispatchChannel=currentDispatchChannel,e){logger.info(`Dispatch joining channel ${e} as speaker`),dispatchSocket.emit("setSpeakerChannel",normalizeFrequency(e));const t={name:dispatchUserState.name,callsign:dispatchUserState.name,nacId:dispatchUserState.nacId,serverId:dispatchUserId};logger.debug(`📤 Sending updateUserInfo on channel change: ${JSON.stringify(t)}`),dispatchSocket.emit("updateUserInfo",t),updateVoiceStatus("listening"),n&&lastUserStates.delete(`channel-${n}`),lastUserStates.delete(`channel-${e}`)}else dispatchSocket.emit("setSpeakerChannel",null),updateVoiceStatus("muted")}function isPTTKeyEvent(e){const t=settings.pttKey.toUpperCase(),n=e.key.toUpperCase(),i=e.code;if(n===t)return!0;if(1===t.length&&/[A-Z]/.test(t))return i===`Key${t}`||n===t;if(1===t.length&&/[0-9]/.test(t))return i===`Digit${t}`||i===`Numpad${t}`||n===t;if(/^F\d+$/.test(t))return i===t||n===t;const a={CONTROL:["ControlLeft","ControlRight"],CTRL:["ControlLeft","ControlRight"],ALT:["AltLeft","AltRight"],SHIFT:["ShiftLeft","ShiftRight"],META:["MetaLeft","MetaRight"],WIN:["MetaLeft","MetaRight"],WINDOWS:["MetaLeft","MetaRight"]};if(a[t])return a[t].includes(i);const o={SPACE:"Space",SPACEBAR:"Space",ENTER:"Enter",RETURN:"Enter",TAB:"Tab",ESCAPE:"Escape",ESC:"Escape",BACKSPACE:"Backspace",DELETE:"Delete",DEL:"Delete",INSERT:"Insert",INS:"Insert",HOME:"Home",END:"End",PAGEUP:"PageUp",PAGEDOWN:"PageDown",ARROWUP:"ArrowUp",UP:"ArrowUp",ARROWDOWN:"ArrowDown",DOWN:"ArrowDown",ARROWLEFT:"ArrowLeft",LEFT:"ArrowLeft",ARROWRIGHT:"ArrowRight",RIGHT:"ArrowRight",CAPSLOCK:"CapsLock",CAPS:"CapsLock"};if(o[t])return i===o[t];if(t.startsWith("NUMPAD")){const e=t.replace("NUMPAD","");if(/^\d$/.test(e))return i===`Numpad${e}`;const n={ADD:"NumpadAdd","+":"NumpadAdd",SUBTRACT:"NumpadSubtract","-":"NumpadSubtract",MULTIPLY:"NumpadMultiply","*":"NumpadMultiply",DIVIDE:"NumpadDivide","/":"NumpadDivide",DECIMAL:"NumpadDecimal",".":"NumpadDecimal",ENTER:"NumpadEnter"};if(n[e])return i===n[e]}const s={GRAVE:"Backquote","`":"Backquote","~":"Backquote",TILDE:"Backquote",MINUS:"Minus","-":"Minus",DASH:"Minus",EQUAL:"Equal","=":"Equal",EQUALS:"Equal",LEFTBRACKET:"BracketLeft","[":"BracketLeft",RIGHTBRACKET:"BracketRight","]":"BracketRight",BACKSLASH:"Backslash","\\":"Backslash",SEMICOLON:"Semicolon",";":"Semicolon",APOSTROPHE:"Quote","'":"Quote",QUOTE:"Quote",COMMA:"Comma",",":"Comma",DOT:"Period",".":"Period",PERIOD:"Period",SLASH:"Slash","/":"Slash"};return!!s[t]&&i===s[t]}function deleteCookie(e){window.cookieStore?cookieStore.delete(e):document.cookie=`${e}=;expires=Thu, 01 Jan 1970 00:00:00 GMT`}document.addEventListener("keydown",e=>{isDesktopApp||!isPTTKeyEvent(e)||e.repeat||"INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName||isBroadcastModalOpen()||pttKeyCapturing||(e.preventDefault(),startPTT())}),document.addEventListener("keyup",e=>{isDesktopApp||!isPTTKeyEvent(e)||"INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName||isBroadcastModalOpen()||pttKeyCapturing||(e.preventDefault(),stopPTT())}),document.addEventListener("mousedown",e=>{if(isDesktopApp||"mouse"!==settings.pttType)return;({0:1,1:3,2:2,3:4,4:5}[e.button]||e.button+1)!==settings.pttMouseButton||"INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName||isBroadcastModalOpen()||pttKeyCapturing||(e.preventDefault(),startPTT())}),document.addEventListener("mouseup",e=>{if(isDesktopApp||"mouse"!==settings.pttType)return;({0:1,1:3,2:2,3:4,4:5}[e.button]||e.button+1)!==settings.pttMouseButton||"INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName||isBroadcastModalOpen()||pttKeyCapturing||(e.preventDefault(),stopPTT())}),document.addEventListener("contextmenu",e=>{"mouse"===settings.pttType&&2===settings.pttMouseButton&&e.preventDefault()}),document.addEventListener("DOMContentLoaded",()=>{UIkit.util.on(document,"show","#alert-type-dropdown",()=>{logger.debug("Alert dropdown show event")}),UIkit.util.on(document,"shown","#alert-type-dropdown",()=>{logger.debug("Alert dropdown shown event")}),UIkit.util.on(document,"hide","#alert-type-dropdown",()=>{logger.debug("Alert dropdown hide event")}),UIkit.util.on(document,"hidden","#alert-type-dropdown",()=>{logger.debug("Alert dropdown hidden event")});const e=document.getElementById("tone-selector");e&&setupToneSelection(e),UIkit.util.on("#broadcast-modal","hidden",()=>{logger.debug("Modal hidden event fired");const e=document.getElementById("tone-selector");if(e)try{UIkit.tab(e).show(0)}catch(t){e.querySelectorAll("li").forEach((e,t)=>{e.classList.toggle("uk-active",0===t)})}document.getElementById("selected-tone").value="none"})});let initializeAppCalled=!1;async function initializeApp(){if(initializeAppCalled)return;if(initializeAppCalled=!0,logger?.info&&logger.info("Initializing app..."),loadSettings(),applyVolumeSettings(),document.getElementById("settings-sfx-volume")&&setupVolumeSliderListeners(),applyTheme(settings.theme),logger.debug(`Sortable available: ${"undefined"!=typeof Sortable}`),"undefined"==typeof Sortable)return logger.error("SortableJS failed to load!"),void requestAnimationFrame(()=>{setTimeout(initializeApp,100)});if(await detectDesktopApp(),isDesktopApp&&desktopEndpoint){const e=document.getElementById("favicon-icon"),t=document.getElementById("favicon-shortcut");e&&(e.href=buildAssetUrl("favicon.png")),t&&(t.href=buildAssetUrl("favicon.png"))}initDispatch();const e=document.getElementById("callsign"),t=document.getElementById("nac-code");e&&e.addEventListener("keypress",e=>{"Enter"===e.key&&(e.preventDefault(),isAuthenticating||authenticate())}),t&&t.addEventListener("keypress",e=>{"Enter"===e.key&&(e.preventDefault(),isAuthenticating||authenticate())});try{let e="/api/health";isDesktopApp&&desktopEndpoint&&(e=`${desktopEndpoint.replace(/\/$/,"")}/api/health`);const t=await fetch(e);if(t.ok){const e=await t.json();if(e.authMethod&&"discord"===e.authMethod){authMethod="discord";const e=document.getElementById("nac-code-container");e&&(e.style.display="none");const t=document.getElementById("auth-submit-text");t&&(t.innerText="Login with Discord")}}}catch(e){logger.debug("Auth method health check failed (desktop first-launch or network issue)")}const n=new URLSearchParams(window.location.search),i=n.get("session"),a=n.get("token"),o=n.get("callsign"),s=n.get("error");if(i&&a){logger.info("Session info found in URL params (Tauri OAuth flow)"),dispatchSessionId=i,dispatchAuthToken=a,o&&(document.getElementById("callsign").value=decodeURIComponent(o));if((await authenticatedFetch("/api/status")).ok){logger.info("OAuth session valid, proceeding to authenticate"),isAuthenticated=!0;const e=window.location.pathname+window.location.hash;window.history.replaceState({},document.title,e),await postAuthenticate()}else{logger.warn("OAuth session invalid");const e=document.getElementById("auth-error");e.innerText="Session validation failed. Please try again.",e.classList.remove("hidden")}}else if("unauthorized"===s){logger.warn("OAuth authorization failed");const e=document.getElementById("auth-error");e.innerText="You're not authorized to use this radio system.",e.classList.remove("hidden")}else if(""!==document.cookie){let e=!1;if(document.cookie.split("; ").forEach(t=>{const n=t.indexOf("="),i=-1===n?t:t.substring(0,n),a=-1===n?"":t.substring(n+1);let o;try{o=decodeURIComponent(a)}catch(e){o=a}switch(i){case"dispatch_callsign":document.getElementById("callsign").value=o;break;case"dispatch_auth_token":dispatchAuthToken=o;break;case"dispatch_session":dispatchSessionId=o;break;case"dispatch_auth_error":if("Unauthorized"===o){const t=document.getElementById("auth-error");t.innerText="You're not authorized to use this radio system.",t.classList.remove("hidden"),e=!0,deleteCookie("dispatch_auth_error")}break;default:logger.warn(`Unknown cookie: ${i}`)}}),!e){(await authenticatedFetch("/api/status")).ok?(logger.warn("Existing session valid, proceeding to authenticate"),isAuthenticated=!0,await postAuthenticate()):(logger.warn("Existing session invalid, waiting for user to authenticate"),deleteCookie("dispatch_session"),deleteCookie("dispatch_auth_token"),deleteCookie("dispatch_callsign"))}}}function initializeMobilePTT(){const e=/Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),t=document.getElementById("pttButton");if(e&&t){logger.debug("Mobile device detected - optimizing PTT button"),t.classList.add("mobile-ptt");t.getAttribute("title");t.setAttribute("title","Touch and hold to talk - Release to stop"),t.setAttribute("data-mobile","true"),t.addEventListener("touchstart",handlePTTTouchStart,{passive:!1}),t.addEventListener("touchend",handlePTTTouchEnd,{passive:!0}),t.addEventListener("touchcancel",handlePTTTouchEnd,{passive:!0}),logger.debug("Mobile PTT optimization complete")}}function setupTooltipDropdownHandlers(){document.addEventListener("show",e=>{if(e.target.classList.contains("uk-dropdown")){document.body.classList.add("dropdown-open");const t=e.target.closest(".user-item, .dispatch-user");t&&t.classList.add("dropdown-active");document.querySelectorAll(".uk-tooltip").forEach(e=>{e.style.display="none",e.style.visibility="hidden",e.style.opacity="0"}),UIkit.tooltip(document).forEach(e=>{try{e.hide()}catch(e){}})}}),document.addEventListener("hide",e=>{e.target.classList.contains("uk-dropdown")&&(setTimeout(()=>{document.body.classList.remove("dropdown-open")},100),document.querySelectorAll(".dropdown-active").forEach(e=>{e.classList.remove("dropdown-active")}))}),document.addEventListener("mouseover",e=>{document.body.classList.contains("dropdown-open")&&(e.stopPropagation(),e.preventDefault())})}function validateAudioState(){try{logger.debug("🩺 [HEALTH CHECK] Starting comprehensive audio state validation");let e=!1;const t=[];let n=!1;const i=[];let a=!1;if((isTransmitting||isPTTActive)&&(n=!0,a=!0,i.push({serverId:dispatchUserId,name:"DISPATCH",channel:currentDispatchChannel}),logger.debug(`🩺 REAL-TIME: We are transmitting (isPTTActive=${isPTTActive}, isTransmitting=${isTransmitting})`)),currentlyTransmittingUser&¤tlyTransmittingUser!==dispatchUserId&&(n=!0,i.push({serverId:currentlyTransmittingUser,name:"OTHER_USER",channel:"UNKNOWN"}),logger.debug(`🩺 REAL-TIME: Someone else is transmitting (currentlyTransmittingUser=${currentlyTransmittingUser})`)),isTransmissionPlaying&&(n=!0,logger.debug(`🩺 REAL-TIME: Transmission sounds are playing (isTransmissionPlaying=${isTransmissionPlaying})`)),!n&&dispatchData&&dispatchData.zones)for(const e of Object.values(dispatchData.zones))if(e.channels)for(const t of Object.values(e.channels))if(t.users)for(const e of Object.values(t.users))if(e.isTalking){n=!0;const o={serverId:e.serverId,name:e.name||e.callsign,channel:t.frequency};i.push(o),logger.debug(`🩺 DISPATCH DATA: User ${e.serverId} is talking on ${t.frequency}`),t.frequency===currentDispatchChannel&&(a=!0)}if(logger.debug(`🩺 [HEALTH CHECK] Anyone talking: ${n}, Talking users: ${JSON.stringify(i)}, isPTTActive: ${isPTTActive}, isTransmitting: ${isTransmitting}, currentlyTransmittingUser: ${currentlyTransmittingUser}, isTransmissionPlaying: ${isTransmissionPlaying}`),!isTransmissionPlaying||n||isPTTActive||isTransmitting||currentlyTransmittingUser||(logger.warn("🔧 [CLEANUP] Found stuck transmission sounds - absolutely no activity detected"),t.push("Stop transmission sounds"),forceStopTransmissionSounds(),e=!0),transmissionAudio.mid&&!1===transmissionAudio.mid.paused&&!n&&!isPTTActive&&!isTransmitting&&!currentlyTransmittingUser){logger.warn("🔧 [CLEANUP] Found stuck transmission mid sound - absolutely no activity"),t.push("Stop transmission mid audio");try{transmissionAudio.mid.pause(),transmissionAudio.mid.currentTime=0}catch(e){logger.debug(`Error stopping transmission mid: ${e}`)}e=!0}let o=0;const s=[];activeBgAudio.forEach((t,a)=>{const[r]=a.split("_"),c=parseInt(r,10);i.some(e=>parseInt(e.serverId,10)===c)||n||isPTTActive||isTransmitting||currentlyTransmittingUser||(logger.warn(`🔧 [CLEANUP] Found stuck background sound: ${a} - absolutely no activity anywhere`),s.push(a),o++,e=!0)}),s.forEach(e=>{const n=activeBgAudio.get(e);if(n){t.push(`Stop background audio: ${e}`);try{n.source&&n.source.stop(),n.animationId&&cancelAnimationFrame(n.animationId)}catch(t){logger.debug(`Error stopping background audio ${e}: ${t}`)}activeBgAudio.delete(e)}}),e?logger.warn(`🔧 [CLEANUP] Audio health check found ${o+(isTransmissionPlaying&&!n?1:0)} stuck sounds. Actions taken: ${t.join(", ")}`):(!validateAudioState.lastCleanLog||Date.now()-validateAudioState.lastCleanLog>3e4)&&(logger.debug(`🩺 [HEALTH CHECK] Audio state is healthy - ${activeBgAudio.size} background sounds, transmission playing: ${isTransmissionPlaying}, anyone talking: ${n}`),validateAudioState.lastCleanLog=Date.now());const r=[];scannedChannels.forEach(e=>{e===currentDispatchChannel&&r.push(e)}),r.forEach(n=>{logger.warn(`🔧 [CLEANUP] Removing invalid scanned channel: ${n} (same as main channel)`),scannedChannels.delete(n);const i=document.getElementById(`scan-toggle-${n}`);i&&(i.classList.remove("bg-accent"),i.setAttribute("data-uk-tooltip","Toggle channel scan")),t.push(`Remove invalid scanned channel: ${n}`),e=!0}),e?logger.warn(`🔧 [CLEANUP SUMMARY] Audio health check found issues. Actions taken: ${t.join(", ")}`):(!validateAudioState.lastSuccessLog||Date.now()-validateAudioState.lastSuccessLog>3e4)&&(logger.debug(`✅ [HEALTH CHECK] Audio state is healthy - ${activeBgAudio.size} background sounds active, transmission: ${isTransmissionPlaying}, anyone talking: ${n}`),validateAudioState.lastSuccessLog=Date.now()),!(activeBgAudio.size>0)||n||isPTTActive||isTransmitting||currentlyTransmittingUser||isTransmissionPlaying?activeBgAudio.size>0&&logger.debug(`🩺 Background sounds active but activity detected - keeping sounds (anyoneTalking=${n}, isPTTActive=${isPTTActive}, isTransmitting=${isTransmitting}, currentlyTransmittingUser=${currentlyTransmittingUser}, isTransmissionPlaying=${isTransmissionPlaying})`):(logger.warn(`🚨 [EMERGENCY CLEANUP] Found ${activeBgAudio.size} background sounds but absolutely no activity detected (anyoneTalking=${n}, isPTTActive=${isPTTActive}, isTransmitting=${isTransmitting}, currentlyTransmittingUser=${currentlyTransmittingUser}, isTransmissionPlaying=${isTransmissionPlaying})`),forceStopAllBackgroundAudio(),e=!0)}catch(e){logger.error(`🩺 [HEALTH CHECK] Error during audio validation: ${e}`)}}function forceStopAllBackgroundAudio(){logger.warn("🚨 [EMERGENCY] Force stopping all background audio"),nuclearDispatchAudioStop(),activeBgAudio.forEach((e,t)=>{try{e.source&&e.source.stop(),e.animationId&&cancelAnimationFrame(e.animationId),logger.debug(`⏹️ Force stopped: ${t}`)}catch(e){logger.debug(`Error force-stopping ${t}: ${e}`)}}),activeBgAudio.clear(),forceStopTransmissionSounds(),currentlyTransmittingUser=null,logger.debug("🧹 Emergency cleanup completed")}function nuclearDispatchAudioStop(){logger.warn("☢️ [NUCLEAR] Emergency stop of all dispatch audio");try{document.querySelectorAll("audio").forEach((e,t)=>{try{e.pause(),e.currentTime=0,logger.debug(`☢️ Stopped HTML audio element ${t}`)}catch(e){logger.debug(`Error stopping HTML audio ${t}: ${e}`)}}),transmissionAudio&&["start","mid","end"].forEach(e=>{if(transmissionAudio[e])try{transmissionAudio[e].pause(),transmissionAudio[e].currentTime=0}catch(t){logger.debug(`Error stopping transmission ${e}: ${t}`)}}),isTransmissionPlaying=!1,currentlyTransmittingUser=null,logger.warn("☢️ [NUCLEAR] Emergency dispatch audio stop completed")}catch(e){logger.error(`☢️ [NUCLEAR] Error during nuclear stop: ${e}`)}}function dispatchAudioDiagnostics(){logger.warn("🔍 [DIAGNOSTICS] Running dispatch audio diagnostics...");let e=0,t=0;try{document.querySelectorAll("audio").forEach((t,n)=>{t.paused||(e++,logger.warn(`🔍 Active HTML audio ${n}: src=${t.src}, currentTime=${t.currentTime}`))}),activeBgAudio&&(t=activeBgAudio.size,activeBgAudio.forEach((e,t)=>{logger.warn(`🔍 Active background audio: ${t}`)}));const n=transmissionAudio&&(transmissionAudio.start||transmissionAudio.mid||transmissionAudio.end);logger.warn(`🔍 Transmission audio active: ${n}`);const i=sirenPlayers?sirenPlayers.size:0,a=heliPlayers?heliPlayers.size:0;logger.warn(`🔍 Siren players: ${i}`),logger.warn(`🔍 Heli players: ${a}`);const o=mediaStream?.active;logger.warn(`🔍 Media stream active: ${o}`),logger.warn(`🔍 [DIAGNOSTICS] Summary: HTML Audio: ${e}, Background: ${t}`),0===e&&0===t?logger.info("✅ [DIAGNOSTICS] No active audio detected - cleanup appears successful"):logger.error("❌ [DIAGNOSTICS] Active audio detected - may need cleanup")}catch(e){logger.error(`🔍 [DIAGNOSTICS] Error during diagnostics: ${e}`)}}function _toggleChannelScan(e){const t=normalizeFrequency(e);if(logger.debug(`📻 toggleChannelScan called: frequency=${e}, normalized=${t}, currentChannel=${currentDispatchChannel}`),t===currentDispatchChannel)return void UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-3'><uk-icon icon='alert-triangle'></uk-icon></span>Cannot scan your main connected channel</div>",status:"warning",timeout:3e3});if(isChannelTrunked(t))return void UIkit.notification({message:"<div class='flex items-center bg-destructive text-destructive-foreground p-3 rounded-lg'><span class='flex-none mr-3'><uk-icon icon='alert-triangle'></uk-icon></span>Trunked channels cannot be scanned</div>",status:"warning",timeout:3e3});const n=scannedChannels.has(t),i=document.getElementById(`scan-toggle-${t}`);logger.debug(`📻 Scan check: isCurrentlyScanned=${n}, scanButton exists=${!!i}`),n?(scannedChannels.delete(t),i.classList.remove("bg-accent"),i.setAttribute("data-uk-tooltip","Toggle channel scan"),dispatchSocket?.connected&&(dispatchSocket.emit("removeListeningChannel",t),logger.debug(`📻 Sent removeListeningChannel to server: ${t}`)),logger.debug(`📻 Removed channel ${t} from scan list`),UIkit.notification({message:`<div class='flex items-center bg-card text-foreground p-3 rounded-lg'><span class='flex-none mr-3'><uk-icon icon='headphones'></uk-icon></span>Stopped scanning ${getChannelName(t)}</div>`,status:"primary",timeout:2e3})):(scannedChannels.add(t),i.classList.add("bg-accent"),i.setAttribute("data-uk-tooltip","Stop scanning channel"),dispatchSocket?.connected&&(dispatchSocket.emit("addListeningChannel",t),logger.debug(`📻 Sent addListeningChannel to server: ${t}`)),logger.debug(`📻 Added channel ${t} to scan list`),UIkit.notification({message:`<div class='flex items-center bg-chart-2 text-foreground p-3 rounded-lg'><span class='flex-none mr-3'><uk-icon icon='headphones'></uk-icon></span>Now scanning ${getChannelName(t)}</div>`,status:"success",timeout:2e3})),logger.debug(`📻 Current scanned channels: [${Array.from(scannedChannels).join(", ")}]`)}function isChannelTrunked(e){if(!dispatchData||!dispatchData.zones)return!1;for(const t of Object.values(dispatchData.zones))if(t.channels)for(const n of Object.values(t.channels))if(normalizeFrequency(n.frequency)===e)return"trunked"===n.type;return!1}function getChannelName(e){if(!dispatchData||!dispatchData.zones)return e;for(const t of Object.values(dispatchData.zones))if(t.channels)for(const n of Object.values(t.channels))if(normalizeFrequency(n.frequency)===e)return`${n.name} (${e})`;return e}function updateChannelScanIndicator(e,t){logger.debug(`🎯 updateChannelScanIndicator called: frequency=${e}, isActive=${t}`);const n=document.getElementById(`channel-container-${e}`);if(n)if(t){n.classList.add("scan-active");if(!n.querySelector(".scan-transmission-indicator")){const t=document.createElement("div");t.className="scan-transmission-indicator",t.textContent="SCANNING";const i=n.querySelector(".channel-header .flex.items-center.gap-3.flex-1");i&&(i.appendChild(t),logger.debug(`🟢 Added SCANNING indicator to channel ${e}`))}logger.debug(`🟢 IMMEDIATE: Channel ${e} scan indicator ON - classes: ${n.className}`)}else{logger.debug(`🔴 AGGRESSIVE CLEANUP: Removing scan visuals for channel ${e}`),n.classList.remove("scan-active");n.querySelectorAll(".scan-transmission-indicator").forEach(t=>{t.remove(),logger.debug(`🔴 Removed scan indicator from channel ${e}`)}),logger.debug(`🔴 IMMEDIATE: Channel ${e} scan indicator OFF - final classes: ${n.className}`)}else logger.warn(`❌ Channel container not found for frequency: ${e}`)}function startTransmissionSounds(){isTransmissionPlaying||(isTransmissionPlaying=!0,logger.debug("🔊 Starting transmission sounds"),transmissionAudio.start&&transmissionAudio.start.pause(),transmissionAudio.mid&&(transmissionAudio.mid.pause(),transmissionAudio.mid.loop=!1),transmissionAudio.end&&transmissionAudio.end.pause(),transmissionAudio.start=new Audio(buildAssetUrl("audio/transStart.wav")),transmissionAudio.start.volume=.4*(window.dispatchSfxVolume||1),transmissionAudio.start.play().catch(e=>logger.debug("Start sound failed:",e)),transmissionAudio.mid=new Audio(buildAssetUrl("audio/transMid.wav")),transmissionAudio.mid.volume=.3*(window.dispatchSfxVolume||1),transmissionAudio.mid.loop=!0,transmissionAudio.mid.play().catch(e=>logger.debug("Mid sound failed:",e)))}function stopTransmissionSounds(){if(logger.debug(`🔊 stopTransmissionSounds called - isTransmissionPlaying: ${isTransmissionPlaying}, transmissionAudio.mid exists: ${!!transmissionAudio.mid}`),isTransmissionPlaying=!1,transmissionAudio.mid){logger.debug("🔊 FORCE stopping transmission mid sound");try{transmissionAudio.mid.pause(),transmissionAudio.mid.currentTime=0,transmissionAudio.mid.loop=!1,transmissionAudio.mid=null,logger.debug("🔊 ✅ Transmission mid sound forcefully stopped")}catch(e){logger.error(`❌ Error stopping transmission mid: ${e}`),transmissionAudio.mid=null}}else logger.debug("🔊 No transmission mid sound to stop");const e=["audio/transEnd.wav","audio/transEnd1.wav","audio/transEnd2.wav"],t=buildAssetUrl(e[Math.floor(Math.random()*e.length)]);logger.debug(`🔊 Creating transmission end sound: ${t}`);const n=new Audio(t);n.volume=.4*(window.dispatchSfxVolume||1),n.addEventListener("loadstart",()=>{logger.debug(`🔊 transEnd loading started: ${t}`)}),n.addEventListener("canplaythrough",()=>{logger.debug(`🔊 transEnd can play through: ${t}`)}),n.addEventListener("error",e=>{logger.error(`❌ Error loading transmission end sound: ${t}`,e)}),n.addEventListener("ended",()=>{logger.debug(`🔊 transEnd finished playing: ${t}`)}),logger.debug(`🔊 Attempting to play transmission end sound: ${t}`),n.play().then(()=>{logger.debug(`✅ Transmission end sound playing: ${t}`)}).catch(e=>{logger.error(`❌ End sound failed to play: ${t}`,e)})}function forceStopTransmissionSounds(){logger.debug("🔊 🚨 FORCE stopping ALL transmission sounds - no conditions"),isTransmissionPlaying=!1,transmissionAudio.start&&(transmissionAudio.start.pause(),transmissionAudio.start.currentTime=0),transmissionAudio.mid&&(transmissionAudio.mid.pause(),transmissionAudio.mid.currentTime=0,transmissionAudio.mid.loop=!1),transmissionAudio={start:null,mid:null,end:null}}window.addEventListener("beforeunload",()=>{healthMonitor.checkInterval&&clearInterval(healthMonitor.checkInterval),forceStopAllBackgroundAudio(),dispatchSocket?.connected&&scannedChannels.size>0&&scannedChannels.forEach(e=>{dispatchSocket.emit("removeListeningChannel",e)}),scannedChannels.clear(),dispatchSocket?.connected&&(dispatchSocket.emit("setSpeakerChannel",null),dispatchSocket.disconnect())}),window.addEventListener("focus",()=>{setTimeout(()=>{validateAudioState()},2e3)}),document.addEventListener("visibilitychange",()=>{document.hidden||setTimeout(()=>{validateAudioState()},2e3)}),setInterval(()=>{!isDispatchConnected&&wasDispatchConnected&&(wasDispatchConnected=!1,savedDispatchChannel=null,dispatchDisconnectionStartTime=null,dispatchReconnectionInterval&&(clearInterval(dispatchReconnectionInterval),dispatchReconnectionInterval=null),Object.values(talkingTimeouts).forEach(clearTimeout),talkingTimeouts={},pttReleaseTimer&&(clearTimeout(pttReleaseTimer),pttReleaseTimer=null))},3e4),ensureTauriLocalURL()?console.log("[Tauri] Waiting for redirect to complete..."):"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{initializeApp().catch(console.error),initializeMobilePTT(),setupTooltipDropdownHandlers()}):(initializeApp(),initializeMobilePTT(),setupTooltipDropdownHandlers()),window.dispatchAudioDiagnostics=dispatchAudioDiagnostics,setInterval(()=>{validateAudioState()},3e4),window.testTransmissionSounds=()=>{logger.debug("🔧 Manual test: Starting transmission sounds"),startTransmissionSounds(),setTimeout(()=>{logger.debug("🔧 Manual test: Stopping transmission sounds"),stopTransmissionSounds()},5e3)},window.testConfig=()=>{logger.debug("🔧 Config test - dispatchData:",dispatchData),logger.debug("🔧 Config test - config:",dispatchData?.config),logger.debug("🔧 Config test - analogTransmissionEffects:",dispatchData?.config?.analogTransmissionEffects)},window.openSettingsModal=_openSettingsModal,window.capturePTTKey=_capturePTTKey,window.saveSettings=_saveSettings,window.previewTheme=_previewTheme,window.applyEndpointConfig=_applyEndpointConfig,window.selectAlertType=_selectAlertType,window.sendBroadcastAlert=_sendBroadcastAlert,window.sendUserAlert=_sendUserAlert,window.confirmChangeCallsign=_confirmChangeCallsign,window.confirmDisconnectUser=_confirmDisconnectUser,window.openBroadcastModal=_openBroadcastModal,window.openUserAlertModal=_openUserAlertModal,window.openChangeCallsignModal=_openChangeCallsignModal,window.openDisconnectModal=_openDisconnectModal,window.toggleZone=_toggleZone,window.toggleChannel=_toggleChannel,window.toggleChannelScan=_toggleChannelScan,window.addEventListener("load",()=>{initializeApp(),initializeMobilePTT(),logger.info("Debug commands available:"),logger.info(" debugBgSounds() - Show background sound status"),logger.info(" stopAllBgSounds() - Stop all background sounds"),logger.info(" testBgSound('siren'|'heli'|'gunshot', serverId) - Test sounds"),logger.info(" testSimpleBgSound('siren'|'heli') - Test simple audio"),logger.info(" checkAudioFiles() - Check if audio files are accessible")})</script> </body> </html> |