Files
Elite-Gaming-FiveM/resources/radio/server/dispatch.html
T
2025-12-05 23:32:20 -08:00

1 line
182 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",function(o){const r=o.error?.message||o.message,e=o.filename+":"+o.lineno+":"+o.colno,n=o.error?.stack;if(!window.logger||!window.logger.warn)return console.error("Global error caught:",r,"at",e),n&&console.error("Stack trace:",n),!1;window.logger.warn("Global error caught:",r,"at",e),n&&window.logger.warn("Stack trace:",n),window.logger.wrapAll&&"function"==typeof window.logger.wrapAll||(console.error("Global error caught:",r,"at",e),n&&console.error("Stack trace:",n))}),window.addEventListener("unhandledrejection",function(o){const r=o.reason;window.logger&&window.logger.warn?(window.logger.warn("Unhandled promise rejection:",r),window.logger.wrapAll&&"function"==typeof window.logger.wrapAll||console.warn("Unhandled promise rejection:",r)):console.warn("Unhandled promise rejection:",r)})</script> <link rel=stylesheet href=https://cdn.jsdelivr.net/npm/franken-ui@2.1.0-next.14/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.6/Sortable.min.js></script> <script src=https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/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!important;background:hsl(var(--muted))!important;border:2px dashed hsl(var(--chart-2))!important;transform:rotate(2deg)}.sortable-chosen{opacity:1!important;transform:scale(1.05);border:2px solid hsl(var(--chart-2));z-index:1000}.sortable-drag{opacity:.8!important;transform:rotate(5deg);box-shadow:0 10px 25px hsl(var(--foreground) / .5);z-index:2000}.sortable-fallback{opacity:.8!important;transform:rotate(5deg);box-shadow:0 10px 25px hsl(var(--foreground) / .5);z-index:2000;cursor:grabbing!important;background:hsl(var(--card))!important;border:2px solid hsl(var(--chart-2))!important}.dispatch-user.sortable-chosen,.dispatch-user.sortable-drag,.user-item.sortable-chosen,.user-item.sortable-drag{z-index:100!important}.channel-users.drag-over{background:hsl(var(--chart-2) / .2)!important;border:2px dashed hsl(var(--chart-2))!important;transition:all .2s ease;box-shadow:inset 0 0 10px hsl(var(--chart-2) / .4)}.uk-dropdown{z-index:10001!important}.uk-tooltip{z-index:8888!important}body.dropdown-open .uk-tooltip,body.dropdown-open [uk-tooltip]:hover::after,body.dropdown-open [uk-tooltip]:hover::before{display:none!important;visibility:hidden!important;opacity:0!important;pointer-events:none!important}body.dropdown-open [title][uk-tooltip]{pointer-events:none!important}.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!important}.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!important;opacity:.6}.other-dispatch{cursor:default!important;opacity:.8}.other-dispatch:hover{cursor:default!important}.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!important;border-radius:.5rem!important}.ptt-button.active,.ptt-button:active{background:hsl(var(--primary))!important;color:hsl(var(--primary-foreground))!important;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))!important}.ptt-button.transmitting{animation:ptt-pulse .5s ease-in-out infinite alternate;background:hsl(var(--primary))!important;border-color:hsl(var(--primary))!important;color:hsl(var(--primary-foreground))!important}.ptt-button.transmitting .h-2,.ptt-button.transmitting .size-4{color:hsl(var(--primary-foreground))!important}@media (max-width:768px){.ptt-button{font-size:16px}}.mobile-ptt{font-weight:500!important}.mobile-ptt:active{transform:scale(.98)!important;background:hsl(var(--chart-2))!important}.ptt-active-mobile{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important;-webkit-touch-callout:none!important;-webkit-tap-highlight-color:transparent!important}@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!important}.dispatch-user .text-xs{font-weight:500!important}.dispatch-user[data-is-my-dispatch=true] .text-sm{color:hsl(var(--chart-1))!important}.dispatch-user[data-is-my-dispatch=true] .text-xs{color:hsl(var(--chart-1))!important}.dispatch-user[data-is-my-dispatch=false] .text-sm{color:hsl(var(--chart-5))!important}.dispatch-user[data-is-my-dispatch=false] .text-xs{color:hsl(var(--chart-5))!important}.other-dispatch{cursor:not-allowed!important;user-select:none;background:hsl(var(--chart-5) / .15)!important;border-color:hsl(var(--chart-5) / .6)!important}.other-dispatch:hover{transform:none!important}.dispatch-user[data-is-my-dispatch=true]{cursor:grab!important;background:hsl(var(--chart-1) / .2)!important;border-color:hsl(var(--chart-1))!important}.dispatch-user[data-is-my-dispatch=true]:hover{transform:translateY(-1px)!important;box-shadow:0 4px 12px hsl(var(--chart-1) / .3)!important;background:hsl(var(--chart-1) / .3)!important}.dispatch-user[data-is-my-dispatch=true]:active{cursor:grabbing!important}.sortable-chosen.dispatch-user[data-is-my-dispatch=true]{transform:rotate(2deg)!important;box-shadow:0 8px 25px hsl(var(--chart-1) / .4)!important;background:hsl(var(--chart-1) / .4)!important}.dispatch-user[data-is-my-dispatch=true].talking{background:hsl(var(--primary) / .6)!important;border-color:hsl(var(--primary))!important;box-shadow:0 0 8px hsl(var(--primary) / .4)!important}.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))!important;font-weight:600!important}.other-dispatch.talking{background:hsl(var(--primary) / .4)!important;border-color:hsl(var(--primary))!important;box-shadow:0 0 8px hsl(var(--primary) / .3)!important}.other-dispatch.talking .text-sm,.other-dispatch.talking .text-xs{color:hsl(var(--primary-foreground))!important;font-weight:600!important}.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!important;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}#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))!important;color:hsl(var(--popover-foreground))!important;border:1px solid hsl(var(--border))!important;border-radius:4px!important;padding:8px 12px!important;font-size:12px!important;max-width:200px!important}#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)!important}.uk-modal-dialog{background:0 0!important}.uk-notification{background:0 0!important}.uk-notification-message{background:0 0!important;border:none!important}.uk-notification-message-danger{background:0 0!important;border:none!important}.uk-notification-message-success{background:0 0!important;border:none!important}: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!important}[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!important;border:2px outset hsl(var(--border))!important}[data-theme=basic] .uk-dropdown{border:2px inset hsl(var(--border))!important;box-shadow:none!important}[data-theme=basic] .uk-btn{border:2px outset hsl(var(--border))!important;border-radius:0!important}[data-theme=basic] .uk-btn.active,[data-theme=basic] .uk-btn:active{border:2px inset hsl(var(--border))!important}[data-theme=basic-dark] .rounded,[data-theme=basic-dark] .rounded-lg,[data-theme=basic-dark] .rounded-xl{border-radius:0!important}[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!important;border:2px outset hsl(var(--border))!important}[data-theme=basic-dark] .uk-dropdown{border:2px inset hsl(var(--border))!important;box-shadow:none!important}[data-theme=basic-dark] .uk-btn{border:2px outset hsl(var(--border))!important;border-radius:0!important}[data-theme=basic-dark] .uk-btn.active,[data-theme=basic-dark] .uk-btn:active{border:2px inset hsl(var(--border))!important}: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!important;align-items:center!important;justify-content:center!important;padding:6px 8px!important;min-width:32px;border-radius:.5rem!important}.scan-toggle-btn:hover{background:hsl(var(--secondary) / .8);transform:scale(1.05)}.scan-toggle-btn.bg-accent{background:hsl(var(--primary))!important;color:hsl(var(--primary-foreground))!important;box-shadow:0 0 15px hsl(var(--primary) / .5);border-color:hsl(var(--primary))}.hamburger-menu-btn:hover{background:hsl(var(--secondary) / .8)!important}.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!important}</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.0-next.14/dist/js/core.iife.js type=module></script> <script src=https://cdn.jsdelivr.net/npm/franken-ui@2.1.0-next.14/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 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 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> <label class="uk-form-label text-foreground font-medium block mb-2" for=settings-callsign> Callsign </label> <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> <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 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=isInitializing||authenticate()> <div class=size-4> <uk-icon icon=unlock></uk-icon> </div> <span>Access Dispatch</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> <a href=# class="px-4 py-2 text-sm">No Tone</a> </li> <li> <a href=# class="px-4 py-2 text-sm">Alert A (Long)</a> </li> <li> <a href=# class="px-4 py-2 text-sm">Alert B (Short)</a> </li> <li> <a href=# class="px-4 py-2 text-sm">Panic/Evac</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=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="v3.2";let logger=window.logger={level:3,fatal:function(...e){e.length>0&&console.error("[FATAL]",...e)},error:function(...e){e.length>0&&console.error("[ERROR]",...e)},warn:function(...e){e.length>0&&console.warn("[WARN]",...e)},log:function(...e){e.length>0&&console.log("[LOG]",...e)},info:function(...e){e.length>0&&console.info("[INFO]",...e)},success:function(...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}};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";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__&&window.__TAURI__.event&&(window.__TAURI__.event.listen("ptt-event",function(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__&&window.__TAURI__.core)try{await window.__TAURI__.core.invoke("update_settings",{newSettings:{endpoint:desktopEndpoint,ptt_key:e,has_configured_endpoint:settings.hasConfiguredEndpoint}}),logger.info("PTT key updated in Tauri backend:",e)}catch(e){logger.warn("Failed to update PTT key in Tauri backend:",e)}}async function notifyEndpointChange(e){if(!(isTauriApp&&window.__TAURI__&&window.__TAURI__.core))throw new Error("Tauri API not available");await window.__TAURI__.core.invoke("update_settings",{newSettings:{endpoint:e,ptt_key:settings.pttKey,has_configured_endpoint:settings.hasConfiguredEndpoint}}),logger.info("Endpoint updated in Tauri backend:",e)}async function loadTauriSettings(){if(isTauriApp&&window.__TAURI__&&window.__TAURI__.core)try{const e=await window.__TAURI__.core.invoke("get_settings");desktopEndpoint=e.endpoint,desktopPTTKey=settings.pttKey,void 0!==e.has_configured_endpoint&&(settings.hasConfiguredEndpoint=e.has_configured_endpoint,saveSettingsToStorage()),e.ptt_key!==settings.pttKey&&(await window.__TAURI__.core.invoke("update_settings",{newSettings:{endpoint:desktopEndpoint,ptt_key:settings.pttKey,has_configured_endpoint:settings.hasConfiguredEndpoint}}),logger.info("Synced PTT key from localStorage to Tauri:",settings.pttKey)),logger.info("Loaded Tauri settings:",{endpoint:desktopEndpoint,pttKey:settings.pttKey,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__&&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",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&&"v3.2"!==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: v3.2, 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&&"v3.2"!==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: v3.2, 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):(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",function(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",function(e){const t=parseInt(e.target.value);settings.sfxVolume=isNaN(t)?50:t,n&&(n.textContent=`(${settings.sfxVolume}%)`),applyVolumeSettings()}),t&&t.addEventListener("uk-input-range:input",function(e){const t=parseInt(e.target.value);settings.voiceVolume=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");e.textContent="...",e.classList.remove("bg-input/50","hover:bg-input"),e.classList.add("bg-accent"),document.addEventListener("keydown",function t(n){n.preventDefault(),n.stopPropagation();const i=n.key.toUpperCase();settings.pttKey=i,e.textContent=i,e.classList.remove("bg-accent"),e.classList.add("bg-input/50","hover:bg-input"),document.removeEventListener("keydown",t,!0),pttKeyCapturing=!1},!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);settings.sfxVolume=isNaN(e)?50:e}if(i){const e=parseInt(i.value);settings.voiceVolume=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&&notifyPTTKeyChange(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&&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 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&&window.__unocss.theme){const t=themeConfigs[e];if(t&&t.fontFamily){const e=window.__unocss.theme.fontFamily[t.fontFamily];e&&(window.__unocss.theme.fontFamily.sans=e)}logger.debug(`UnoCSS theme updated for: ${e}`)}}let scannedChannels=new Set,scanAudioMuted=!1;function isChannelMonitored(e){const t=normalizeFrequency(e),n=currentDispatchChannel&&normalizeFrequency(currentDispatchChannel)===t,i=scannedChannels.has(t);return n||i}let dispatchData={config:null,channels:{},users:{},zones:{},panicStatus:{},lastServerTimestamp:null,tones:null,persistentAlerts:{}},isAuthenticated=!1,notifiedPanics=new Set,lastUserStates=new Map,isDragging=!1,notificationsShown=new Set,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,audioProcessingInterval=null,isTransmitting=!1,isListening=!1,toneAudioContext=null,toneVolume=.1,dispatchUserState={name:"Dispatch",nacId:"DISPATCH",isTalking:!1,isInPanic:!1},talkingTimeouts=new Map,isInitializing=!1,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){if(isDesktopApp&&desktopEndpoint&&desktopEndpoint!==window.location.origin&&settings.hasConfiguredEndpoint){if(!await checkEndpointHealth())return void showEndpointConfigModal(!1)}settings.callsign&&(document.getElementById("callsign").value=settings.callsign),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)}else showEndpointConfigModal(!0)}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();return e.version&&"v3.2"!==e.version?(healthMonitor.serverVersion=e.version,healthMonitor.hasVersionMismatch=!0,logger.warn(`Version mismatch detected - Panel: v3.2, Server: ${e.version}`)):healthMonitor.hasVersionMismatch=!1,"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://75.139.128.185: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 '),isInitializing=!1}async function authenticate(){if(isInitializing)return;isInitializing=!0;const e=document.getElementById("auth-submit-btn");e&&(e.disabled=!0,e.innerHTML='\n <div class="size-4">\n <uk-icon icon="loader-2" class="animate-spin"></uk-icon>\n </div>\n <span>Authenticating...</span>\n ');const t=document.getElementById("callsign").value,n=document.getElementById("nac-code").value,i=document.getElementById("auth-error");if(!t.trim())return i.classList.remove("hidden"),i.textContent="Please enter a callsign",void resetAuthButton();if(!n.trim())return i.classList.remove("hidden"),i.textContent="Please enter a Network Access Code",void resetAuthButton();try{let e="/radio/dispatch/auth";isDesktopApp&&desktopEndpoint&&(e=`${desktopEndpoint.replace(/\/$/,"")}/radio/dispatch/auth`,logger.debug(`Desktop app auth URL: ${e}`));const a=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({nacId:n,callsign:t.trim()})});if(a.ok){const e=await a.json();dispatchSessionId=e.sessionId,dispatchAuthToken=e.authToken,isAuthenticated=!0,UIkit.modal("#auth-modal").hide(),i.classList.add("hidden"),dispatchSocket&&dispatchSocket.connected&&dispatchSessionId&&dispatchSocket.emit("setDispatchSession",dispatchSessionId);for(let e=localStorage.length-1;e>=0;e--){const t=localStorage.key(e);t&&t.startsWith("dispatchSession")&&localStorage.removeItem(t)}const o=Date.now(),s=n.substring(0,4).toUpperCase();dispatchSessionName=t.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,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})),isInitializing=!1},1e3)}else notificationsShown.clear(),i.classList.remove("hidden"),i.textContent="Invalid Network Access Code",document.getElementById("callsign").value="",document.getElementById("nac-code").value="",resetAuthButton()}catch(e){notificationsShown.clear(),logger.error("Authentication failed: "+e),i.classList.remove("hidden"),i.textContent="Authentication failed. Please try again.",document.getElementById("callsign").value="",document.getElementById("nac-code").value="",resetAuthButton()}}async function requestConfigData(){try{const e=await authenticatedFetch("/radio/dispatch/config");dispatchData.config=await e.json(),void 0!==dispatchData.config.logLevel?(logger.level=dispatchData.config.logLevel,logger.level=4,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");dispatchData.tones=await e.json(),logger.debug("Loaded tones data:",Object.keys(dispatchData.tones||{}));const t=Object.keys(dispatchData.tones||{})[0];t&&logger.debug(`Sample tone config for ${t}:`,dispatchData.tones[t]),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";const n=document.createElement("a");n.href="#",n.className="px-4 py-2 text-sm",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"),i=document.createElement("a");i.href="#",i.className="px-4 py-2 text-sm";const a=t.replace(/_/g," ").replace(/\b\w/g,e=>e.toUpperCase());i.textContent=a,n.appendChild(i),e.appendChild(n)}}),setTimeout(()=>{UIkit.tab(e,{animation:"uk-animation-fade"}),setupToneSelection()},100)}}function setupToneSelection(){const e=document.getElementById("tone-selector");if(!e)return;UIkit.util.on(e,"beforeshow",function(e){const t=e.target.textContent.trim();let n="none";if("No Tone"!==t&&(n=t.toUpperCase().replace(/ /g,"_"),dispatchData.tones&&!dispatchData.tones[n])){const e=t.toUpperCase().replace(/[^A-Z0-9]/g,"_");dispatchData.tones[e]?(n=e,logger.debug(`Mapped tone "${t}" to "${e}"`)):logger.warn(`Tone "${n}" not found in available tones:`,Object.keys(dispatchData.tones||{}))}const i=document.getElementById("selected-tone");i&&(i.value=n,logger.debug(`Tone selection updated: ${n}`))});e.querySelectorAll("a").forEach(e=>{e.addEventListener("click",function(e){const t=this.textContent.trim();let n="none";if("No Tone"!==t&&(n=t.toUpperCase().replace(/ /g,"_"),dispatchData.tones&&!dispatchData.tones[n])){const e=t.toUpperCase().replace(/[^A-Z0-9]/g,"_");dispatchData.tones[e]?(n=e,logger.debug(`Mapped tone "${t}" to "${e}"`)):logger.warn(`Tone "${n}" not found in available tones:`,Object.keys(dispatchData.tones||{}))}const i=document.getElementById("selected-tone");i&&(i.value=n,logger.debug(`Tone selection updated via click: ${n}`))})})}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&&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=>{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"}">\n <div class="size-4 mr-2" style="color: ${t.color||"#059669"}">\n <uk-icon icon="alert-triangle"></uk-icon>\n </div>\n ${t.name}${t.toneOnly?" (Tone)":""}\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: v3.2, 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: v3.2, 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&&"v3.2"!==n.version?(healthMonitor.serverVersion=n.version,healthMonitor.hasVersionMismatch=!0,logger.warn(`Version mismatch detected - Panel: v3.2, Server: ${n.version}`)):n.version&&(healthMonitor.hasVersionMismatch=!1),n.channels&&Object.entries(n.channels).forEach(([e,t])=>{t.speakers&&t.speakers.forEach(t=>{t.serverId<0&&logger.debug(`📊 Server dispatch user data: freq=${e}, serverId=${t.serverId}, name="${t.name}", nacId="${t.nacId}"`)}),t.listeners&&t.listeners.forEach(t=>{t.serverId<0&&logger.debug(`📊 Server dispatch user data: freq=${e}, serverId=${t.serverId}, name="${t.name}", nacId="${t.nacId}"`)})});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,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;const s=dispatchData.channels||{};dispatchData.channels=n.channels||{},dispatchData.users=n.users||{},dispatchData.panicStatus=n.panicStatus||{},dispatchData.activeAlerts=n.activeAlerts||{};const r=Object.keys(dispatchData.channels).length,c=Object.keys(s).length;r!==c&&logger.debug(`Channel count changed: ${c} -> ${r}`),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(){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&&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&&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&&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&&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||[],m=g.length,h=p.length;(m>0||h>0)&&(logger.trace(`Channel ${a}: ${m} speakers, ${h} listeners`),i=!0),r.textContent=`${m} user${1!==m?"s":""}`,c.textContent=`${h} listener${1!==h?"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 v=document.getElementById("statusChannelCount");if(v){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&&dispatchData.persistentAlerts[t];n||i||e++}}),v.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&&dispatchData.activeAlerts[n],r=dispatchData.panicStatus&&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&&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){n.querySelectorAll("li").forEach((e,t)=>{0===t?e.classList.add("uk-active"):e.classList.remove("uk-active")}),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&&dispatchData.config.alerts&&(a=Object.values(dispatchData.config.alerts).find(e=>e.name===t));const o=normalizeFrequency(e);if(a&&a.isPersistent){if(dispatchData.persistentAlerts[o])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]={name:t,color:a?a.color:null,isPersistent:!!a&&a.isPersistent,tone:a?a.tone:null}}else a||"General Alert"===t||logger.warn(`Unknown alert type: ${t}, treating as non-persistent`);logger.info(`Sending broadcast: ${t} to ${o} MHz with tone: ${i||a?.tone||"none"}`);try{const s=i&&"none"!==i?i:a?.tone;if(await authenticatedFetch("/radio/dispatch/broadcast",{method:"POST",body:JSON.stringify({frequency:o,type:t,message:n,tone:s,isPersistent:!!a&&a.isPersistent,color:a?a.color:null})}),logger.info("Broadcast alert sent successfully"),s&&"none"!==s){logger.debug(`Sending tone request: ${s} to frequency: ${o}`);try{const e=await authenticatedFetch("/radio/dispatch/tone",{method:"POST",body:JSON.stringify({frequency:o,tone:s})});if(logger.debug(`Tone response status: ${e.status}`),e.ok){const t=await e.json();logger.debug(`Tone sent successfully: ${JSON.stringify(t)}`)}else{const t=await e.text();logger.error(`Tone request failed: ${t}`)}}catch(e){logger.error(`Failed to send tone: ${e}`)}isChannelMonitored(o)?dispatchData.tones&&dispatchData.tones[s]?(logger.debug(`Broadcast: Playing tone locally for dispatcher: ${s} (monitoring channel ${o})`),playDispatchTone(s)):logger.warn(`Broadcast: Cannot play tone ${s} - tones available:`,Object.keys(dispatchData.tones||{})):logger.debug(`Broadcast: Not playing tone ${s} locally - not monitoring channel ${o}`)}else 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()}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})}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"}async function toggleAlert(e,t,n=null){e=normalizeFrequency(e);let i=null;if(dispatchData.config&&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;!a&&i.tone&&isChannelMonitored(e)?dispatchData.tones&&dispatchData.tones[i.tone]?(logger.debug(`Toggle Alert: Playing tone locally for dispatcher: ${i.tone} (monitoring channel ${e})`),playDispatchTone(i.tone)):logger.warn(`Toggle Alert: Cannot play tone ${i.tone} - tones available:`,Object.keys(dispatchData.tones||{})):!a&&i.tone&&logger.debug(`Toggle Alert: Not playing tone ${i.tone} locally - not monitoring channel ${e}`);try{const n=a?"/radio/dispatch/alert/clear":"/radio/dispatch/alert/trigger";await authenticatedFetch(n,{method:"POST",body:JSON.stringify({frequency:e,alertType:t,alertConfig:i})}),a?delete dispatchData.activeAlerts[e]:dispatchData.activeAlerts[e]=i,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&&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&&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.tone,o=normalizeFrequency(e);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='alert-triangle'></uk-icon></span> ${t} sent to ${e} MHz</div>`,status:"success",timeout:3e3}),a&&isChannelMonitored(e)?dispatchData.tones&&dispatchData.tones[a]?(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}`)}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})}if(n&&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={beep:"BEEP",boop:"BOOP",chirp:"CHIRP",panic:"PANIC",alert_a:"ALERT_A",alert_b:"ALERT_B",alert_c:"ALERT_C"}[t]||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:t})}),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&&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),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 currentDisconnectData={};function openDisconnectModal(e,t){document.querySelectorAll("[data-uk-dropdown]").forEach(e=>{UIkit.dropdown(e).hide(!1)}),currentDisconnectData={userId:parseInt(e),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))){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))||a.includes(t.toString())||a.includes(t),r=dispatchData.panicStatus[e]||{},c=Array.isArray(r)?r.includes(t)||r.includes(parseInt(t))||r.map(e=>e.toString()).includes(t.toString()):r.hasOwnProperty(t)||r.hasOwnProperty(t.toString())||r.hasOwnProperty(parseInt(t))||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&&dispatchData.activeAlerts[e]||dispatchData.panicStatus&&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 <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")}let 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",animation:150,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:function(e){if(e.item.classList.contains("other-dispatch"))return logger.warn("Preventing selection of other dispatch user"),!1},onStart:function(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:function(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:function(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),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,maxDispatchReconnectionAttempts=5,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>=maxDispatchReconnectionAttempts)return logger.error("Max dispatch reconnection attempts reached"),void showDispatchNotification("Dispatch connection could not be restored","error");dispatchReconnectionInterval||(dispatchReconnectionAttempts++,logger.info(`Dispatch reconnection attempt ${dispatchReconnectionAttempts}/${maxDispatchReconnectionAttempts}`),dispatchReconnectionInterval=setInterval(async()=>{try{logger.info(`Dispatch reconnection attempt ${dispatchReconnectionAttempts}`),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>=maxDispatchReconnectionAttempts)clearInterval(dispatchReconnectionInterval),dispatchReconnectionInterval=null,showDispatchNotification("Dispatch connection could not be restored","error");else{dispatchReconnectionAttempts++;(dispatchDisconnectionStartTime?Date.now()-dispatchDisconnectionStartTime:0)>5e3&&showDispatchNotification(`Dispatch reconnecting... (${dispatchReconnectionAttempts}/${maxDispatchReconnectionAttempts})`,"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 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),(dispatchReconnectionAttempts>0||dispatchDisconnectionStartTime)&&setTimeout(()=>{restoreDispatchState(),updateVoiceStatus("listening")},500)}),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"),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&&currentlyTransmittingUser&&currentlyTransmittingUser!==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&&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&&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),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&&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&&dispatchData.config.analogTransmissionEffects&&(logger.debug("🔊 Stopping transmission sounds for scanned channel"),stopTransmissionSounds()),currentlyTransmittingUser===e.serverId&&(currentlyTransmittingUser=null),stopBackgroundSoundsForUser(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&&dispatchData.tones[e.tone]?(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",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&&dispatchData.tones[e.tone]?(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("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),!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),!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&&e.serverId){let t=!1;currentDispatchChannel&&currentlyTransmittingUser===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&&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)})}async function setupAudioCapture(){try{mediaStream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:!0,noiseSuppression:!0,autoGainControl:!0,channelCount:1}});try{audioContext=new(window.AudioContext||window.webkitAudioContext)({latencyHint:"interactive"}),"suspended"===audioContext.state&&await audioContext.resume(),logger.info("Audio context initialized successfully")}catch(e){logger.error("Failed to create audio context:",e),audioContext=null}setupMediaRecorder(),logger.info("Audio capture initialized with optimized settings"),logger.debug(`Audio context state: ${audioContext?audioContext.state:"null"}`)}catch(e){logger.error("Failed to access microphone: "+e),logger.info("Continuing without microphone - user can still listen to radio");try{audioContext=new(window.AudioContext||window.webkitAudioContext)({latencyHint:"interactive"}),"suspended"===audioContext.state&&await audioContext.resume(),logger.info("Audio context initialized successfully for listening only")}catch(e){logger.error("Failed to create audio context:",e),audioContext=null}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 setupMediaRecorder(){if(logger.debug(`🎙️ setupMediaRecorder called - mediaStream exists: ${!!mediaStream}`),mediaStream){logger.debug(`🎙️ mediaStream active: ${mediaStream.active}, tracks: ${mediaStream.getTracks().length}`);try{mediaRecorder=new MediaRecorder(mediaStream,{mimeType:"audio/webm; codecs=opus",audioBitsPerSecond:16e3});let e=[];mediaRecorder.ondataavailable=t=>{t.data.size>0&&e.push(t.data)},mediaRecorder.onstop=async()=>{if(logger.debug("MediaRecorder stopped, audioChunks.length: "+e.length),e.length>0){const t=await processAudioChunks(e);if(logger.debug("Processed audio, base64Audio length: "+(t?t.length:0)),!t||0===t.trim().length)return logger.debug("Skipping transmission - empty base64 audio data"),void(e=[]);logger.debug("currentDispatchChannel: "+currentDispatchChannel),logger.debug("dispatchSocket connected: "+(dispatchSocket&&dispatchSocket.connected)),t&&t.trim().length>0&&currentDispatchChannel?(logger.debug("Sending voice data: channel="+currentDispatchChannel+", serverId="+dispatchUserId+", dataLength="+t.length),dispatchSocket.emit("voice",{channelName:currentDispatchChannel,serverId:dispatchUserId,data:t}),logger.debug("Voice data sent via socket.emit")):logger.debug("NOT sending voice - base64Audio: "+!!t+" (length: "+(t?t.length:0)+") currentDispatchChannel: "+currentDispatchChannel)}else logger.debug("No audio chunks to process");e=[]},mediaRecorder.onstart=()=>{e=[]},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=i.result.split(",")[1];t(e)},i.onerror=n,i.readAsDataURL(e)})}function startRecording(){if(logger.debug("startRecording called - mediaRecorder: "+!!mediaRecorder+" state: "+(mediaRecorder?mediaRecorder.state:"no recorder")),logger.debug(`🎙️ MediaRecorder debug: exists=${!!mediaRecorder}, state=${mediaRecorder?mediaRecorder.state:"N/A"}, isTransmitting=${isTransmitting}, mediaStream=${!!mediaStream}`),!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"),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(),isTransmitting?recordCycle():logger.debug("Transmission stopped - ending record cycle"))},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 startPTT(){if(logger.debug(`startPTT called - currentDispatchChannel: ${currentDispatchChannel}, isPTTActive: ${isPTTActive}, mediaStream: ${!!mediaStream}`),currentDispatchChannel&&!isPTTActive)if(mediaStream)if(touchPTTActive&&event&&event.type&&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&&currentlyTransmittingUser&&currentlyTransmittingUser!==dispatchUserId&&(logger.debug(`🔊 🚨 PRIORITY: Stopping scan transmission sounds from user ${currentlyTransmittingUser}`),stopTransmissionSounds()),activeBgAudio.forEach((e,t)=>{const[n]=t.split("_"),i=parseInt(n);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 n||(logger.warn(`Tone "${e}" not found in API tones data. Available tones:`,Object.keys(dispatchData.tones)),null)}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))}async function playDispatchTone(e){logger.debug(`Attempting to play tone: ${e}`);const t=getToneConfig(e);if(t)return 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&&dispatchData.config.analogTransmissionEffects&&startTransmissionSounds(),checkAndStartBackgroundSounds(currentlyTransmittingUser))},100),setTimeout(()=>{validateAudioState()},2e3)):logger.debug("PTT release timer was cancelled - user re-keyed before delay expired")},350))}function playReceivedAudioWithRadioEffects(e,t=!1){if(audioContext){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)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)return void logger.debug("Audio decode returned null buffer");if(0===t.length)return void logger.debug("Audio buffer has zero length");const n=audioContext.createBufferSource();n.buffer=t;const i=audioContext.createGain(),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(),p=audioContext.createGain();a.threshold.value=-22,a.knee.value=0,a.ratio.value=10,a.attack.value=5e-4,a.release.value=.03,o.threshold.value=-18,o.knee.value=0,o.ratio.value=12,o.attack.value=5e-4,o.release.value=.03,s.threshold.value=-6,s.knee.value=0,s.ratio.value=20,s.attack.value=5e-4,s.release.value=.005,r.type="highpass",r.frequency.value=500,r.Q.value=4,c.type="lowpass",c.frequency.value=1800,c.Q.value=6,d.curve=function(){const e=44100,t=new Float32Array(e);for(let n=0;n<e;n++){const i=2*n/e-1;Math.abs(i)<.3?t[n]=i*(1+.35*i*i):t[n]=Math.sign(i)*(.3+.4*Math.tanh(5*(Math.abs(i)-.3)))}return t}(),d.oversample="2x",u.type="lowpass",u.frequency.value=2200,u.Q.value=.7,g.type="highshelf",g.frequency.value=1500,g.Q.value=.7,g.gain.value=-4,l.type="peaking",l.frequency.value=800,l.Q.value=1.5,l.gain.value=3,i.gain.value=1.8,p.gain.value=.5*(window.dispatchVoiceVolume||1),n.connect(i),i.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(p),p.connect(audioContext.destination);try{n.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)}else logger.debug("No voice data to play")}else logger.debug("No audio context available for received audio")}let activeBgAudio=new Map,currentlyTransmittingUser=null,transmissionAudio={start:null,mid:null,end:null},isTransmissionPlaying=!1;function checkAndStartBackgroundSounds(e){if(!e)return;const t=parseInt(e);if(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,m=Math.random()*Math.PI*2,h=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+m)+.25*Math.sin(.25*e*Math.PI+h)+.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)},v=requestAnimationFrame(f);activeBgAudio.set(a,{source:o,animationId:v,outputGain:l}),i||o.addEventListener("ended",()=>{v&&cancelAnimationFrame(v),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);if(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);if(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=.4;null!=t&&(o=Math.max(.1,Math.min(.4,.4*(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),dispatchSocket&&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);dispatchSocket.emit("setSpeakerChannel",null),scannedChannels.has(e)&&(dispatchSocket.emit("addListeningChannel",e),logger.debug(`📻 Re-added ${e} as listening channel (was main, now scanned)`)),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")}async function initializeApp(){if(logger&&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(),isInitializing||authenticate())}),t&&t.addEventListener("keypress",e=>{"Enter"===e.key&&(e.preventDefault(),isInitializing||authenticate())})}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",function(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",function(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",function(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,t=[],n=!1,i=[],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&&currentlyTransmittingUser!==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);i.some(e=>parseInt(e.serverId)===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&&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&&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&&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}}document.addEventListener("keydown",e=>{if(isDesktopApp)return;const t=`Key${settings.pttKey}`;e.code!==t||e.repeat||"INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName||isBroadcastModalOpen()||pttKeyCapturing||(e.preventDefault(),startPTT())}),document.addEventListener("keyup",e=>{if(isDesktopApp)return;const t=`Key${settings.pttKey}`;e.code!==t||"INPUT"===e.target.tagName||"TEXTAREA"===e.target.tagName||isBroadcastModalOpen()||pttKeyCapturing||(e.preventDefault(),stopPTT())}),document.addEventListener("DOMContentLoaded",function(){UIkit.util.on(document,"show","#alert-type-dropdown",function(){logger.debug("Alert dropdown show event")}),UIkit.util.on(document,"shown","#alert-type-dropdown",function(){logger.debug("Alert dropdown shown event")}),UIkit.util.on(document,"hide","#alert-type-dropdown",function(){logger.debug("Alert dropdown hide event")}),UIkit.util.on(document,"hidden","#alert-type-dropdown",function(){logger.debug("Alert dropdown hidden event")});const e=document.getElementById("tone-selector");e&&e.addEventListener("click",function(t){if("A"===t.target.tagName){t.preventDefault();const n=["none","alert_a","alert_b","panic"],i=t.target.closest("li"),a=Array.from(e.querySelectorAll("li")).indexOf(i);if(a>=0&&a<n.length){const e=n[a];document.getElementById("selected-tone").value=e,logger.debug(`Tone selection updated: ${e} at index: ${a}`),logger.debug(`Hidden input value: ${document.getElementById("selected-tone").value}`)}}}),UIkit.util.on("#broadcast-modal","hidden",function(){logger.debug("Modal hidden event fired");const e=document.getElementById("tone-selector");if(e){e.querySelectorAll("li").forEach((e,t)=>{0===t?e.classList.add("uk-active"):e.classList.remove("uk-active")})}})}),window.addEventListener("beforeunload",()=>{healthMonitor.checkInterval&&clearInterval(healthMonitor.checkInterval),forceStopAllBackgroundAudio(),dispatchSocket&&dispatchSocket.connected&&scannedChannels.size>0&&scannedChannels.forEach(e=>{dispatchSocket.emit("removeListeningChannel",e)}),scannedChannels.clear(),dispatchSocket&&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),"loading"===document.readyState?document.addEventListener("DOMContentLoaded",()=>{initializeApp().catch(console.error),initializeMobilePTT(),setupTooltipDropdownHandlers()}):(initializeApp(),initializeMobilePTT(),setupTooltipDropdownHandlers()),window.dispatchAudioDiagnostics=dispatchAudioDiagnostics,setInterval(()=>{validateAudioState()},3e4),window.testTransmissionSounds=function(){logger.debug("🔧 Manual test: Starting transmission sounds"),startTransmissionSounds(),setTimeout(()=>{logger.debug("🔧 Manual test: Stopping transmission sounds"),stopTransmissionSounds()},5e3)},window.testConfig=function(){logger.debug("🔧 Config test - dispatchData:",dispatchData),logger.debug("🔧 Config test - config:",dispatchData?.config),logger.debug("🔧 Config test - analogTransmissionEffects:",dispatchData?.config?.analogTransmissionEffects)},window.addEventListener("load",()=>{isAuthenticated||(logger.debug("Backup initialization trigger"),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>