From 86aaa7933955c1744baa39f6fbf74b696ed6524e Mon Sep 17 00:00:00 2001 From: Gronod Date: Wed, 20 May 2026 23:48:10 +0100 Subject: [PATCH] refactor: Complete technical debt remediation - final cleanup Deleted obsolete public/app.js (replaced by Vite build) All backend services extracted and dashboard.js slimmed to 284 lines Frontend fully migrated to vanilla ES modules All duplications eliminated Comprehensive tests added Vite build wired into Dockerfile --- public/app.js | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 public/app.js diff --git a/public/app.js b/public/app.js deleted file mode 100644 index 36d3c39..0000000 --- a/public/app.js +++ /dev/null @@ -1,42 +0,0 @@ -const o={currentUser:null,downloads:[],downloadClients:[],selectedDownloadClients:[],isAdmin:!1,showAll:!1,csrfToken:null,historyDays:7,historyRefreshHandle:null,ignoreAvailable:!1,lastHistoryItems:[],sseSource:null,sseReconnectTimer:null,statusRefreshHandle:null,webhookSectionExpanded:!1,webhookLoading:!1,sonarrWebhook:{enabled:!1,triggers:{onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1},stats:null},radarrWebhook:{enabled:!1,triggers:{onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1},stats:null},webhookMetrics:null},ie=1200,ce=5*60*1e3,de=5e3;async function ue(){try{const[e,t]=await Promise.all([fetch("/api/auth/me"),fetch("/api/auth/csrf")]),n=await e.json(),s=await t.json();return s.csrfToken&&(o.csrfToken=s.csrfToken),n.authenticated?(o.currentUser=n.user,o.isAdmin=!!n.user.isAdmin,{authenticated:!0,user:n.user}):{authenticated:!1}}catch(e){return console.error("Authentication check failed:",e),{authenticated:!1}}}async function me(e,t,n){try{const a=await(await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:t,rememberMe:n})})).json();return a.success?(o.currentUser=a.user,o.isAdmin=!!a.user.isAdmin,a.csrfToken&&(o.csrfToken=a.csrfToken),{success:!0,user:a.user}):{success:!1,error:a.error||"Login failed"}}catch(s){return console.error(s),{success:!1,error:"Login failed. Please try again."}}}async function he(){try{return await fetch("/api/auth/logout",{method:"POST",headers:csrfToken?{"X-CSRF-Token":csrfToken}:{}}),currentUser=null,csrfToken=null,{success:!0}}catch(e){return console.error("Logout failed:",e),{success:!1}}}async function pe(e=!1){try{const t=new URLSearchParams({days:o.historyDays});o.showAll&&t.set("showAll","true"),e&&t.set("_t",Date.now());const n=await fetch(`/api/history/recent?${t}`);if(!n.ok)throw new Error(`HTTP ${n.status}`);return{success:!0,history:(await n.json()).history||[]}}catch(t){return console.error("[History] Load error:",t),{success:!1,error:"Failed to load history."}}}async function fe(e){try{const t=await fetch("/api/dashboard/blocklist-search",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":csrfToken},body:JSON.stringify({arrQueueId:e.arrQueueId,arrType:e.arrType,arrInstanceUrl:e.arrInstanceUrl,arrInstanceKey:e.arrInstanceKey,arrContentId:e.arrContentId,arrContentType:e.arrContentType})});if(!t.ok){const n=await t.json().catch(()=>({}));throw new Error(n.error||`HTTP ${t.status}`)}return{success:!0}}catch(t){throw console.error("[Blocklist] Error:",t),t}}async function ge(){try{return(await(await fetch("/health")).json()).version||null}catch{return null}}async function ye(){try{const e=await fetch("/api/dashboard/webhook-metrics");return e.ok?await e.json():null}catch{return null}}async function N(){var e,t;try{const n=ye();let s=!1,a={onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1};try{const p=await fetch("/api/sonarr/notifications");if(p.ok){const d=(await p.json()).find(f=>f.name==="Sofarr");s=!!d,d&&(a={onGrab:d.onGrab,onDownload:d.onDownload,onImport:d.onImport,onUpgrade:d.onUpgrade})}}catch{}let i=!1,c={onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1};try{const p=await fetch("/api/radarr/notifications");if(p.ok){const d=(await p.json()).find(f=>f.name==="Sofarr");i=!!d,d&&(c={onGrab:d.onGrab,onDownload:d.onDownload,onImport:d.onImport,onUpgrade:d.onUpgrade})}}catch{}o.webhookMetrics=await n;const m=o.webhookMetrics?Object.entries(o.webhookMetrics.instances||{}):[],u=((e=m.find(([p])=>p.includes("sonarr")))==null?void 0:e[1])||null,l=((t=m.find(([p])=>p.includes("radarr")))==null?void 0:t[1])||null;return o.sonarrWebhook={enabled:s,triggers:a,stats:u},o.radarrWebhook={enabled:i,triggers:c,stats:l},{success:!0}}catch(n){return console.error("Failed to fetch webhook status:",n),{success:!1}}}async function be(){try{if(!(await fetch("/api/sonarr/notifications/sofarr-webhook",{method:"POST",headers:{"X-CSRF-Token":csrfToken||""}})).ok)throw new Error("Failed to enable");return await N(),{success:!0}}catch(e){return console.error("Failed to enable Sonarr webhook:",e),{success:!1,error:e.message}}}async function ve(){try{if(!(await fetch("/api/radarr/notifications/sofarr-webhook",{method:"POST",headers:{"X-CSRF-Token":o.csrfToken||""}})).ok)throw new Error("Failed to enable");return await N(),{success:!0}}catch(e){return console.error("Failed to enable Radarr webhook:",e),{success:!1,error:e.message}}}async function Ee(){try{const e=await fetch("/api/sonarr/notifications");if(!e.ok)throw new Error("Failed to fetch notifications");const n=(await e.json()).find(a=>a.name==="Sofarr");if(!n)throw new Error("Sofarr webhook not found");if(!(await fetch("/api/sonarr/notifications/test",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":o.csrfToken||""},body:JSON.stringify(n)})).ok)throw new Error("Test failed");return await N(),{success:!0}}catch(e){return console.error("Failed to test Sonarr webhook:",e),{success:!1,error:e.message}}}async function ke(){try{const e=await fetch("/api/radarr/notifications");if(!e.ok)throw new Error("Failed to fetch notifications");const n=(await e.json()).find(a=>a.name==="Sofarr");if(!n)throw new Error("Sofarr webhook not found");if(!(await fetch("/api/radarr/notifications/test",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":o.csrfToken||""},body:JSON.stringify(n)})).ok)throw new Error("Test failed");return await N(),{success:!0}}catch(e){return console.error("Failed to test Radarr webhook:",e),{success:!1,error:e.message}}}async function we(){try{const e=await fetch("/api/dashboard/status");if(!e.ok)throw new Error("Failed to fetch status: "+e.status);return{success:!0,data:await e.json()}}catch(e){return console.error("[Status] Error fetching status:",e),{success:!1,error:e.message}}}function Se(e){if(!e)return"N/A";if(typeof e=="string")return e;const t=["B","KB","MB","GB","TB"],n=Math.floor(Math.log(e)/Math.log(1024));return Math.round(e/Math.pow(1024,n)*100)/100+" "+t[n]}function Ce(e){if(!e||e===0)return"0 B/s";const t=["B/s","KB/s","MB/s","GB/s"];let n=e,s=0;for(;n>=1024&&s{const a="S"+String(s.season).padStart(2,"0")+"E"+String(s.episode).padStart(2,"0");return s.title?a+" — "+s.title:a});t.setAttribute("data-tooltip",n.join(` -`))}return t}function R(e,t,n){const s=document.createDocumentFragment();if(t&&e&&e.length>0){const a=e.filter(c=>!c.matchedUser),i=e.filter(c=>c.matchedUser);for(const c of a){const m=document.createElement("span");m.className="download-user-badge unmatched",m.textContent=c.label,s.appendChild(m)}for(const c of i){const m=document.createElement("span");m.className="download-user-badge",m.textContent=c.matchedUser,s.appendChild(m)}}else if(n){const a=document.createElement("span");a.className="download-user-badge",a.textContent=n,s.appendChild(a)}return s}function X(e){const t=document.createElement("span");t.className="download-client-logo-wrapper download-card-logo-wrapper";const n=document.createElement("img");return n.className="download-client-logo",n.src=`/images/clients/${e.client}.svg`,n.alt=`${e.instanceName||e.client} icon`,n.title=e.instanceName||e.client,n.onerror=()=>{t.textContent=e.client.charAt(0).toUpperCase(),t.classList.add("fallback")},t.appendChild(n),t}function Q(){const e=document.getElementById("downloads-list"),t=document.getElementById("no-downloads");let n=o.downloads;if(o.selectedDownloadClients.length>0){const i=o.selectedDownloadClients.map(c=>o.downloadClients[c]).filter(Boolean);n=o.downloads.filter(c=>i.some(m=>m.type===c.client&&m.id===c.instanceId))}if(o.downloadClients.length>0){const i=new Map(o.downloadClients.map((c,m)=>[c.id,m]));n=[...n].sort((c,m)=>{const u=i.get(c.instanceId)??1/0,l=i.get(m.instanceId)??1/0;return u-l})}if(n.length===0){t.style.display="block",e.innerHTML="";return}t.style.display="none";const s=new Map;e.querySelectorAll(".download-card").forEach(i=>{s.set(i.dataset.id,i)});const a=new Set;n.forEach(i=>{const c=i.title;a.add(c);const m=s.get(c);if(m)Be(m,i);else{const u=xe(i);e.appendChild(u)}}),s.forEach((i,c)=>{a.has(c)||i.remove()})}function Be(e,t){const n=e.querySelector(".download-header-right");n&&n.remove(),e.querySelectorAll(".download-header .download-user-badge").forEach(r=>r.remove());const a=e.querySelector(".download-header .download-client-logo-wrapper");a&&a.remove();const i=e.querySelector(".download-card-logo-wrapper");i&&i.remove();const c=e.querySelector(".download-header");if(c&&!c.querySelector(".download-header-right")){const r=document.createElement("div");r.className="download-header-right";const d=R(t.tagBadges,o.showAll,t.matchedUserTag);r.appendChild(d),c.appendChild(r)}t.client&&!e.querySelector(".download-card-logo-wrapper")&&e.appendChild(X(t));const m=e.querySelector(".download-status");m&&m.textContent!==t.status&&(m.textContent=t.status,m.className=`download-status ${t.status}`);const u=e.querySelector(".progress-container");if(u&&t.progress!==void 0){const r=u.querySelector(".progress-bar"),d=u.querySelector(".progress-text"),f=u.querySelector(".missing-text");if(r){const g=r.querySelector(".downloaded");g&&(g.style.width=t.progress+"%")}if(d&&(d.textContent=t.progress+"%"),f){const g=parseFloat(t.mb)||parseFloat(t.size),y=parseFloat(t.mbmissing)||0;y>0&&g>0?f.textContent=`(missing ${y.toFixed(1)} of ${g.toFixed(1)} MB)`:f.textContent=""}}const l=e.querySelector('.detail-item[data-label="Speed"] .detail-value');l&&t.speed!==void 0&&(l.textContent=t.speed);const p=e.querySelector('.detail-item[data-label="ETA"] .detail-value');if(p&&t.eta!==void 0&&(p.textContent=t.eta),t.qbittorrent){const r=e.querySelector('.detail-item[data-label="Seeds"] .detail-value');r&&t.seeds!==void 0&&(r.textContent=t.seeds);const d=e.querySelector('.detail-item[data-label="Peers"] .detail-value');d&&t.peers!==void 0&&(d.textContent=t.peers);const f=e.querySelector('.detail-item[data-label="Availability"]');f&&t.availability!==void 0&&(f.querySelector(".detail-value").textContent=`${t.availability}%`,f.classList.toggle("availability-warning",parseFloat(t.availability)<100))}}async function Te(e,t){if(confirm(`Blocklist "${t.title}" and trigger a new search? - -This will: -• Remove the download from the download client -• Add this release to the blocklist -• Trigger an automatic search for a new release`)){e.disabled=!0,e.textContent="⏳ Working…";try{await fe(t),e.textContent="✓ Done — searching…",e.className="blocklist-search-btn success"}catch(n){console.error("[Blocklist] Error:",n),e.disabled=!1,e.textContent="⛔ Blocklist & Search",e.className="blocklist-search-btn error",e.title=`Failed: ${n.message}`,setTimeout(()=>{e.className="blocklist-search-btn",e.title="Remove this release from the download client, add it to the blocklist, and trigger a new automatic search"},4e3)}}}function xe(e){const t=document.createElement("div");if(t.className=`download-card ${e.type}`,t.dataset.id=e.title,e.coverArt){const r=document.createElement("div");r.className="download-cover";const d=document.createElement("img");d.src=e.coverArt?"/api/dashboard/cover-art?url="+encodeURIComponent(e.coverArt):"",d.alt=e.movieName||e.seriesName||e.title,d.loading="lazy",r.appendChild(d),t.appendChild(r)}const n=document.createElement("div");n.className="download-info";const s=document.createElement("div");s.className="download-header";const a=document.createElement("span");if(a.className=`download-type ${e.type}`,e.type==="series")a.textContent="📺 Series";else if(e.type==="movie")a.textContent="🎬 Movie";else if(e.type==="torrent"){const r=e.instanceName?` (${e.instanceName})`:"";a.textContent=`📥 Torrent${r}`}else a.textContent=e.type;const i=document.createElement("span");if(i.className=`download-status ${e.status}`,i.textContent=e.status,s.appendChild(a),s.appendChild(i),e.importIssues&&e.importIssues.length>0){const r=document.createElement("span");r.className="import-issue-badge",r.textContent="Import Pending",r.setAttribute("data-tooltip",e.importIssues.join(` -`)),s.appendChild(r)}if((o.isAdmin||e.canBlocklist)&&e.arrQueueId){const r=document.createElement("button");r.className="blocklist-search-btn",r.textContent="⛔ Blocklist & Search",r.title="Remove this release from the download client, add it to the blocklist, and trigger a new automatic search",r.addEventListener("click",()=>Te(r,e)),s.appendChild(r)}const c=document.createElement("div");c.className="download-header-right";const m=R(e.tagBadges,o.showAll,e.matchedUserTag);c.appendChild(m),s.appendChild(c),e.client&&t.appendChild(X(e));const u=document.createElement("h3");if(u.className="download-title",u.textContent=e.title,n.appendChild(s),n.appendChild(u),e.seriesName){const r=document.createElement("p");r.className="download-series",o.isAdmin&&e.arrLink?r.innerHTML='Series: '+v(e.seriesName)+"":r.textContent=`Series: ${e.seriesName}`,n.appendChild(r);const d=K(e.episodes);d&&n.appendChild(d)}if(e.movieName){const r=document.createElement("p");r.className="download-movie",o.isAdmin&&e.arrLink?r.innerHTML='Movie: '+v(e.movieName)+"":r.textContent=`Movie: ${e.movieName}`,n.appendChild(r)}const l=document.createElement("div");l.className="download-details";const p=C("Size",Se(e.size));if(l.appendChild(p),e.progress!==void 0){const r=document.createElement("div");r.className="detail-item progress-item",r.dataset.label="Progress";const d=document.createElement("span");d.className="detail-label",d.textContent="Progress";const f=document.createElement("div");f.className="progress-container";const g=parseFloat(e.mb)||parseFloat(e.size),y=parseFloat(e.mbmissing)||0,T=parseFloat(e.progress)||0,I=document.createElement("div");if(I.className="progress-bar",T>0){const b=document.createElement("div");b.className="progress-segment downloaded",b.style.width=T+"%",I.appendChild(b)}f.appendChild(I);const B=document.createElement("span");if(B.className="progress-text",B.textContent=e.progress+"%",f.appendChild(B),e.client&&(e.client==="qbittorrent"||e.client==="rtorrent")&&y>0&&g>0){const b=document.createElement("span");b.className="missing-text",b.textContent=`(missing ${y.toFixed(1)} of ${g.toFixed(1)} MB)`,f.appendChild(b)}r.appendChild(d),r.appendChild(f),l.appendChild(r)}if(e.speed&&e.speed>0){const r=C("Speed",Ce(e.speed));l.appendChild(r)}if(e.eta){const r=C("ETA",e.eta);l.appendChild(r)}if(e.qbittorrent){if(e.seeds!==void 0){const r=C("Seeds",e.seeds);l.appendChild(r)}if(e.peers!==void 0){const r=C("Peers",e.peers);l.appendChild(r)}if(e.availability!==void 0){const r=C("Availability",`${e.availability}%`);parseFloat(e.availability)<100&&r.classList.add("availability-warning"),l.appendChild(r)}}if(e.completedAt){const r=C("Completed",Le(e.completedAt));l.appendChild(r)}if(o.isAdmin&&(e.downloadPath||e.targetPath)){const r=document.createElement("div");if(r.className="download-paths",e.downloadPath){const d=document.createElement("div");d.className="path-item",d.innerHTML='Download: '+v(e.downloadPath)+"",r.appendChild(d)}if(e.targetPath){const d=document.createElement("div");d.className="path-item",d.innerHTML='Target: '+v(e.targetPath)+"",r.appendChild(d)}l.appendChild(r)}return n.appendChild(l),t.appendChild(n),t}function C(e,t){const n=document.createElement("div");n.className="detail-item",n.dataset.label=e;const s=document.createElement("span");s.className="detail-label",s.textContent=e;const a=document.createElement("span");return a.className="detail-value",a.textContent=t,n.appendChild(s),n.appendChild(a),n}function Le(e){return e?new Date(e).toLocaleString():"N/A"}function F(){V();const e=o.showAll?"?showAll=true":"",t=new EventSource("/api/dashboard/stream"+e);o.sseSource=t;let n=!0;t.onmessage=s=>{try{const a=JSON.parse(s.data);if(o.currentUser=a.user,o.isAdmin=!!a.isAdmin,o.downloads=a.downloads,a.downloadClients){o.downloadClients=a.downloadClients;const i=new CustomEvent("downloadClientsUpdated");document.dispatchEvent(i)}document.getElementById("currentUser").textContent=o.currentUser||"-",Q(),nt(),n&&(n=!1,st())}catch(a){console.error("[SSE] Failed to parse message:",a)}},t.onerror=()=>{console.warn("[SSE] Connection lost, browser will retry...")},console.log("[SSE] Stream connected")}function V(){o.sseSource&&(o.sseSource.close(),o.sseSource=null,console.log("[SSE] Stream closed"))}function Ne(e){o.showAll=e,F();const t=new CustomEvent("historyReload");document.dispatchEvent(t)}(function(){const t=localStorage.getItem("sofarr-download-client");if(t&&t!=="all")try{o.selectedDownloadClients=[t],localStorage.setItem("sofarr-download-clients",JSON.stringify(o.selectedDownloadClients)),localStorage.removeItem("sofarr-download-client")}catch(n){console.error("[Migration] Failed to migrate download client filter:",n)}else try{const n=localStorage.getItem("sofarr-download-clients");o.selectedDownloadClients=n?JSON.parse(n):[]}catch(n){console.error("[Migration] Failed to load download client filter:",n),o.selectedDownloadClients=[]}})();(function(){try{const t=localStorage.getItem("sofarr-history-days");t&&(o.historyDays=parseInt(t,10)||7)}catch(t){console.error("[Storage] Failed to load history days:",t)}})();(function(){try{const t=localStorage.getItem("sofarr-ignore-available");o.ignoreAvailable=t==="true"}catch(t){console.error("[Storage] Failed to load ignore available:",t)}})();function De(e){localStorage.setItem("sofarr-history-days",e)}function Me(e){localStorage.setItem("sofarr-ignore-available",e)}function Ae(e){localStorage.setItem("sofarr-download-clients",JSON.stringify(e))}function Y(){return localStorage.getItem("sofarr-theme")||"light"}function Re(e){localStorage.setItem("sofarr-theme",e)}function Fe(){return localStorage.getItem("sofarr-active-tab")||"downloads"}function j(e){localStorage.setItem("sofarr-active-tab",e)}function $e(){const e=document.getElementById("history-days"),t=document.getElementById("history-refresh-btn"),n=document.getElementById("ignore-available-toggle");e&&e.addEventListener("change",()=>{const s=parseInt(e.value,10);s>0&&s<=90&&(historyDays=s,De(s),L(!0))}),t&&t.addEventListener("click",()=>L(!0)),n&&(n.checked=ignoreAvailable,n.addEventListener("change",()=>{ignoreAvailable=n.checked,Me(ignoreAvailable),ee(lastHistoryItems)})),document.addEventListener("historyReload",()=>{L(!0)})}function He(){Z(),o.historyRefreshHandle=setInterval(()=>L(),ce)}function Z(){o.historyRefreshHandle&&(clearInterval(o.historyRefreshHandle),o.historyRefreshHandle=null)}function We(){o.lastHistoryItems=[],document.getElementById("history-list").innerHTML="",document.getElementById("no-history").style.display="none",document.getElementById("history-error").style.display="none"}async function L(e=!1){document.getElementById("history-list");const t=document.getElementById("history-loading"),n=document.getElementById("history-error"),s=document.getElementById("no-history");t.style.display="block",n.style.display="none",s.style.display="none";try{const a=await pe(e);t.style.display="none",a.success?(o.lastHistoryItems=a.history,ee(o.lastHistoryItems)):(n.textContent=a.error||"Failed to load history.",n.style.display="block")}catch(a){t.style.display="none",n.textContent="Failed to load history.",n.style.display="block",console.error("[History] Load error:",a)}}function ee(e){const t=document.getElementById("history-list"),n=document.getElementById("no-history");t.innerHTML="";const s=o.ignoreAvailable?e.filter(a=>!(a.outcome==="failed"&&a.availableForUpgrade)):e;if(!s.length){n.style.display="block";return}n.style.display="none",s.forEach(a=>t.appendChild(Ue(a)))}function Ue(e){const t=document.createElement("div");if(t.className=`history-card ${e.type} ${e.outcome}`,e.coverArt){const l=document.createElement("div");l.className="history-cover";const p=document.createElement("img");p.src="/api/dashboard/cover-art?url="+encodeURIComponent(e.coverArt),p.alt=e.movieName||e.seriesName||e.title,p.loading="lazy",l.appendChild(p),t.appendChild(l)}const n=document.createElement("div");n.className="history-info";const s=document.createElement("div");s.className="history-card-header";const a=document.createElement("span");a.className=`history-type-badge ${e.type}`,a.textContent=e.type==="series"?"📺 Series":"🎬 Movie",s.appendChild(a);const i=document.createElement("span");if(i.className=`history-outcome-badge ${e.outcome}`,i.textContent=e.outcome==="imported"?"✓ Imported":"✗ Failed",s.appendChild(i),e.availableForUpgrade){const l=document.createElement("span");l.className="history-upgrade-badge",l.title="A previous version of this item is available. An upgrade download has failed.",l.textContent="⬆ Available",s.appendChild(l)}if(e.instanceName){const l=document.createElement("span");l.className="history-instance-badge",l.textContent=e.instanceName,s.appendChild(l)}const c=R(e.tagBadges,o.showAll,e.matchedUserTag);s.appendChild(c),n.appendChild(s);const m=document.createElement("h3");if(m.className="history-title",m.textContent=e.title,n.appendChild(m),e.seriesName){const l=document.createElement("p");l.className="history-media-name",o.isAdmin&&e.arrLink?l.innerHTML='Series: '+v(e.seriesName)+"":l.textContent="Series: "+e.seriesName,n.appendChild(l);const p=K(e.episodes);p&&n.appendChild(p)}if(e.movieName){const l=document.createElement("p");l.className="history-media-name",o.isAdmin&&e.arrLink?l.innerHTML='Movie: '+v(e.movieName)+"":l.textContent="Movie: "+e.movieName,n.appendChild(l)}const u=document.createElement("div");if(u.className="history-details",e.completedAt&&u.appendChild(G("Completed",Ie(e.completedAt))),e.quality&&u.appendChild(G("Quality",e.quality)),e.outcome==="failed"&&e.failureMessage){const l=document.createElement("div");l.className="history-failure-message",l.textContent=e.failureMessage,u.appendChild(l)}return n.appendChild(u),t.appendChild(n),t}function G(e,t){const n=document.createElement("div");n.className="detail-item",n.dataset.label=e;const s=document.createElement("span");s.className="detail-label",s.textContent=e;const a=document.createElement("span");return a.className="detail-value",a.textContent=t,n.appendChild(s),n.appendChild(a),n}function Pe(){document.getElementById("webhooks-section")&&(document.getElementById("webhooks-header").addEventListener("click",qe),document.getElementById("enable-sonarr-webhook").addEventListener("click",je),document.getElementById("enable-radarr-webhook").addEventListener("click",Ge),document.getElementById("test-sonarr-webhook").addEventListener("click",_e),document.getElementById("test-radarr-webhook").addEventListener("click",ze))}function qe(){o.webhookSectionExpanded=!o.webhookSectionExpanded;const e=document.getElementById("webhooks-content"),t=document.getElementById("webhooks-toggle");e.style.display=o.webhookSectionExpanded?"":"none",t.classList.toggle("expanded",o.webhookSectionExpanded),o.webhookSectionExpanded&&te()}async function te(){const e=document.getElementById("webhook-loading");e.style.display="";try{(await N()).success&&Oe()}catch(t){console.error("Failed to fetch webhook status:",t)}finally{e.style.display="none"}}function Oe(){const e=document.getElementById("sonarr-status"),t=document.getElementById("enable-sonarr-webhook"),n=document.getElementById("test-sonarr-webhook"),s=document.getElementById("sonarr-triggers"),a=document.getElementById("sonarr-stats");e.textContent=sonarrWebhook.enabled?"● Enabled":"○ Disabled",e.className="status-indicator "+(sonarrWebhook.enabled?"enabled":"disabled"),t.style.display=sonarrWebhook.enabled?"none":"",n.style.display=sonarrWebhook.enabled?"":"none",s.style.display=sonarrWebhook.enabled?"":"none",sonarrWebhook.enabled&&(document.getElementById("sonarr-onGrab").textContent=sonarrWebhook.triggers.onGrab?"✓":"✗",document.getElementById("sonarr-onGrab").className="trigger-value "+(sonarrWebhook.triggers.onGrab?"active":"inactive"),document.getElementById("sonarr-onDownload").textContent=sonarrWebhook.triggers.onDownload?"✓":"✗",document.getElementById("sonarr-onDownload").className="trigger-value "+(sonarrWebhook.triggers.onDownload?"active":"inactive"),document.getElementById("sonarr-onImport").textContent=sonarrWebhook.triggers.onImport?"✓":"✗",document.getElementById("sonarr-onImport").className="trigger-value "+(sonarrWebhook.triggers.onImport?"active":"inactive"),document.getElementById("sonarr-onUpgrade").textContent=sonarrWebhook.triggers.onUpgrade?"✓":"✗",document.getElementById("sonarr-onUpgrade").className="trigger-value "+(sonarrWebhook.triggers.onUpgrade?"active":"inactive")),sonarrWebhook.stats?(a.style.display="",document.getElementById("sonarr-events").textContent=sonarrWebhook.stats.eventsReceived??0,document.getElementById("sonarr-polls").textContent=sonarrWebhook.stats.pollsSkipped??0,document.getElementById("sonarr-last").textContent=O(sonarrWebhook.stats.lastWebhookTimestamp)):a.style.display="none";const i=document.getElementById("radarr-status"),c=document.getElementById("enable-radarr-webhook"),m=document.getElementById("test-radarr-webhook"),u=document.getElementById("radarr-triggers"),l=document.getElementById("radarr-stats");i.textContent=radarrWebhook.enabled?"● Enabled":"○ Disabled",i.className="status-indicator "+(radarrWebhook.enabled?"enabled":"disabled"),c.style.display=radarrWebhook.enabled?"none":"",m.style.display=radarrWebhook.enabled?"":"none",u.style.display=radarrWebhook.enabled?"":"none",radarrWebhook.enabled&&(document.getElementById("radarr-onGrab").textContent=radarrWebhook.triggers.onGrab?"✓":"✗",document.getElementById("radarr-onGrab").className="trigger-value "+(radarrWebhook.triggers.onGrab?"active":"inactive"),document.getElementById("radarr-onDownload").textContent=radarrWebhook.triggers.onDownload?"✓":"✗",document.getElementById("radarr-onDownload").className="trigger-value "+(radarrWebhook.triggers.onDownload?"active":"inactive"),document.getElementById("radarr-onImport").textContent=radarrWebhook.triggers.onImport?"✓":"✗",document.getElementById("radarr-onImport").className="trigger-value "+(radarrWebhook.triggers.onImport?"active":"inactive"),document.getElementById("radarr-onUpgrade").textContent=radarrWebhook.triggers.onUpgrade?"✓":"✗",document.getElementById("radarr-onUpgrade").className="trigger-value "+(radarrWebhook.triggers.onUpgrade?"active":"inactive")),radarrWebhook.stats?(l.style.display="",document.getElementById("radarr-events").textContent=radarrWebhook.stats.eventsReceived??0,document.getElementById("radarr-polls").textContent=radarrWebhook.stats.pollsSkipped??0,document.getElementById("radarr-last").textContent=O(radarrWebhook.stats.lastWebhookTimestamp)):l.style.display="none"}async function je(){k(!0);try{const e=await be();e.success||(console.error("Failed to enable Sonarr webhook:",e.error),alert("Failed to enable Sonarr webhook. Check console for details."))}catch(e){console.error("Failed to enable Sonarr webhook:",e),alert("Failed to enable Sonarr webhook. Check console for details.")}finally{k(!1)}}async function Ge(){k(!0);try{const e=await ve();e.success||(console.error("Failed to enable Radarr webhook:",e.error),alert("Failed to enable Radarr webhook. Check console for details."))}catch(e){console.error("Failed to enable Radarr webhook:",e),alert("Failed to enable Radarr webhook. Check console for details.")}finally{k(!1)}}async function _e(){k(!0);try{const e=await Ee();e.success?alert("Sonarr webhook test sent successfully!"):(console.error("Failed to test Sonarr webhook:",e.error),alert("Failed to test Sonarr webhook. Check console for details."))}catch(e){console.error("Failed to test Sonarr webhook:",e),alert("Failed to test Sonarr webhook. Check console for details.")}finally{k(!1)}}async function ze(){k(!0);try{const e=await ke();e.success?alert("Radarr webhook test sent successfully!"):(console.error("Failed to test Radarr webhook:",e.error),alert("Failed to test Radarr webhook. Check console for details."))}catch(e){console.error("Failed to test Radarr webhook:",e),alert("Failed to test Radarr webhook. Check console for details.")}finally{k(!1)}}function k(e){o.webhookLoading=e,document.getElementById("enable-sonarr-webhook").disabled=e,document.getElementById("enable-radarr-webhook").disabled=e,document.getElementById("test-sonarr-webhook").disabled=e,document.getElementById("test-radarr-webhook").disabled=e,document.getElementById("webhook-loading").style.display=e?"":"none"}async function Je(){const e=document.getElementById("status-panel"),t=document.getElementById("webhooks-section");if(e.style.display!=="none"){e.style.display="none",t&&(t.style.display="none"),o.statusRefreshHandle&&(clearInterval(o.statusRefreshHandle),o.statusRefreshHandle=null);return}e.style.display="block",t&&o.isAdmin?(t.style.display="block",o.webhookSectionExpanded=!1,document.getElementById("webhooks-content").style.display="none",document.getElementById("webhooks-toggle").classList.remove("expanded"),await te()):t&&(t.style.display="none"),_(),o.statusRefreshHandle&&clearInterval(o.statusRefreshHandle),o.statusRefreshHandle=setInterval(_,de)}function Ke(){document.getElementById("status-panel").style.display="none";const e=document.getElementById("webhooks-section");e&&(e.style.display="none"),o.statusRefreshHandle&&(clearInterval(o.statusRefreshHandle),o.statusRefreshHandle=null)}async function _(){var n;const e=document.getElementById("status-panel"),t=document.getElementById("status-content");if(console.log("[Status] panel found:",!!e,"contentDiv found:",!!t,"panel display:",(n=e==null?void 0:e.style)==null?void 0:n.display),!(!e||e.style.display==="none")){console.log("[Status] Refreshing status panel...");try{const s=await we();s.success&&(console.log("[Status] Got status data, rendering..."),Xe(s.data,e))}catch(s){console.error("[Status] Error fetching status:",s),t&&(!t.innerHTML||t.innerHTML.includes("status-loading"))&&(t.innerHTML='

Failed to load status: '+s.message+"

")}}}function Xe(e,t){var I,B,b,$,H,W,U,P,q;console.log("[Status] renderStatusPanel called with data:",e?"yes":"no","keys:",e?Object.keys(e):"none");const n=e.server,s=Math.floor(n.uptimeSeconds/3600),a=Math.floor(n.uptimeSeconds%3600/60),i=n.uptimeSeconds%60,c=`${s}h ${a}m ${i}s`,m=(e.cache.totalSizeBytes/1024).toFixed(1);let u=` -
-

Server Status

- -
-
-
-
Server
-
Uptime${c}
-
Node${D(n.nodeVersion)}
-
Memory (RSS)${n.memoryUsageMB} MB
-
Heap${n.heapUsedMB} / ${n.heapTotalMB} MB
-
-
-
Data Refresh
`;const l=e.polling.intervalMs,r=(e.clients||[]).filter(h=>h.type==="sse");e.polling.enabled?u+=`
Background poll${l/1e3}s
`:u+='
Background pollDisabled
';const d=r.length>0?'SSE push':e.polling.enabled?"Background":"On-demand (idle)";u+=`
Delivery mode${d}
`,u+=`
SSE clients${r.length}
`;for(const h of r){const w=Math.round((Date.now()-h.connectedAt)/1e3);u+=`
${D(h.user)}connected ${w}s ago
`}if(u+="
",o.isAdmin&&e.webhooks){const h=e.webhooks,w=(I=h.sonarr)!=null&&I.enabled?"●":"○",E=(B=h.radarr)!=null&&B.enabled?"●":"○",S=((b=h.sonarr)==null?void 0:b.eventsReceived)||0,oe=(($=h.radarr)==null?void 0:$.eventsReceived)||0,re=((H=h.sonarr)==null?void 0:H.pollsSkipped)||0,le=((W=h.radarr)==null?void 0:W.pollsSkipped)||0;u+=` -
-
Webhooks
-
Sonarr${w} ${(U=h.sonarr)!=null&&U.enabled?"Enabled":"Disabled"}
-
Radarr${E} ${(P=h.radarr)!=null&&P.enabled?"Enabled":"Disabled"}
-
EventsS:${S} R:${oe}
-
Polls skippedS:${re} R:${le}
-
`}const f=e.polling.lastPoll;if(f){const h=Math.round((Date.now()-new Date(f.timestamp).getTime())/1e3);u+=` -
-
Last Poll (${f.totalMs}ms total, ${h}s ago)
-
`;const w=f.tasks.reduce((E,S)=>Math.max(E,S.ms),1);for(const E of f.tasks){const S=Math.max(2,E.ms/w*100);u+=` -
- ${D(E.label)} -
- ${E.ms}ms -
`}u+="
"}u+=` -
-
Cache (${e.cache.entryCount} entries, ${m} KB)
- - - `;for(const h of e.cache.entries){const w=h.sizeBytes>1024?(h.sizeBytes/1024).toFixed(1)+" KB":h.sizeBytes+" B",E=h.expired?'expired':(h.ttlRemainingMs/1e3).toFixed(0)+"s",S=h.itemCount!==null?h.itemCount:"—";u+=``}u+="
KeyItemsSizeTTL
${D(h.key)}${S}${w}${E}
";const g=document.getElementById("status-content"),y=document.getElementById("status-panel");console.log("[Status] contentDiv found:",!!g,"panel children:",(q=y==null?void 0:y.children)==null?void 0:q.length,"HTML length:",u.length),y&&console.log("[Status] panel innerHTML preview:",y.innerHTML.substring(0,200)),g?(g.innerHTML=u,console.log("[Status] HTML rendered, contentDiv innerHTML length:",g.innerHTML.length)):console.error("[Status] contentDiv not found!");const T=document.getElementById("status-close-btn");T&&T.addEventListener("click",Ke),t.querySelectorAll(".timing-bar[data-w]").forEach(h=>{h.style.width=h.dataset.w+"%"})}function D(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}function Qe(){return new Promise(e=>{const t=document.getElementById("login-container");t.classList.add("fade-out"),t.addEventListener("transitionend",()=>{t.style.display="none",t.classList.remove("fade-out"),e()},{once:!0})})}function Ve(){const e=document.getElementById("splash-screen");e.style.display="flex",e.style.opacity="1",e.classList.remove("fade-out")}function M(e){return new Promise(t=>{const n=Date.now()-(e||0),s=Math.max(0,ie-n);setTimeout(()=>{const a=document.getElementById("splash-screen");a.classList.add("fade-out");const c=setTimeout(()=>{a.style.display="none",t()},400+100);a.addEventListener("transitionend",()=>{clearTimeout(c),a.style.display="none",t()},{once:!0})},s)})}async function Ye(){const e=Date.now();try{(await ue()).authenticated?(ne(),se(),F(),await M(e)):(await M(e),A())}catch(t){console.error("Authentication check failed:",t),await M(e),A()}}async function Ze(e){e.preventDefault();const t=document.getElementById("username").value,n=document.getElementById("password").value,s=document.getElementById("remember-me").checked;try{const a=await me(t,n,s);if(a.success){await Qe(),Ve(),await new Promise(c=>requestAnimationFrame(()=>requestAnimationFrame(c))),ne(),se();const i=Date.now();F(),await M(i)}else z(a.error||"Login failed")}catch(a){z("Login failed. Please try again."),console.error(a)}}async function et(){try{V(),Z(),statusRefreshHandle&&(clearInterval(statusRefreshHandle),statusRefreshHandle.value=null),await he(),currentUser=null,We(),A()}catch(e){console.error("Logout failed:",e)}}function A(){document.getElementById("login-container").style.display="flex",document.getElementById("dashboard-container").style.display="none",tt()}function ne(){document.getElementById("login-container").style.display="none",document.getElementById("dashboard-container").style.display="block",document.getElementById("currentUser").textContent=o.currentUser.name||"-";const e=document.getElementById("status-panel");e.style.display="none";const t=document.getElementById("webhooks-section");t&&(t.style.display="none"),document.getElementById("admin-controls").style.display=o.isAdmin?"flex":"none";const n=document.getElementById("history-days");n&&(n.value=o.historyDays),He()}function z(e){const t=document.getElementById("login-error");t.textContent=e,t.style.display="block"}function tt(){const e=document.getElementById("login-error");e.style.display="none"}function nt(){const e=document.getElementById("error-message");e.style.display="none"}function se(){const e=document.getElementById("loading");e.style.display="block"}function st(){const e=document.getElementById("loading");e.style.display="none"}function at(){const e=document.getElementById("download-client-filter-btn"),t=document.getElementById("download-client-filter-dropdown"),n=document.getElementById("download-client-filter-close");!e||!t||(e.addEventListener("click",s=>{s.stopPropagation(),t.style.display=t.style.display==="block"?"none":"block"}),n.addEventListener("click",()=>{t.style.display="none"}),document.addEventListener("click",s=>{!t.contains(s.target)&&s.target!==e&&(t.style.display="none")}),document.addEventListener("downloadClientsUpdated",J),J())}function J(){const e=document.getElementById("download-client-filter-list");e&&(e.innerHTML="",o.downloadClients.forEach((t,n)=>{const s=document.createElement("div");s.className="filter-item",s.dataset.index=n;const a=document.createElement("input");a.type="checkbox",a.id=`client-${n}`,a.checked=o.selectedDownloadClients.includes(n),a.addEventListener("change",()=>ot(n));const i=document.createElement("label");i.htmlFor=`client-${n}`,i.textContent=t.name||`${t.type} (${t.id})`,s.appendChild(a),s.appendChild(i),e.appendChild(s)}),ae())}function ot(e){const t=o.selectedDownloadClients.indexOf(e);t>-1?o.selectedDownloadClients.splice(t,1):o.selectedDownloadClients.push(e),Ae(o.selectedDownloadClients),ae(),Q()}function ae(){const e=document.getElementById("download-client-filter-count");e&&(o.selectedDownloadClients.length===0?e.textContent="All":e.textContent=o.selectedDownloadClients.length)}(function(){const t=Y();t&&document.documentElement.setAttribute("data-theme",t)})();function rt(){const e=document.getElementById("theme-toggle");e&&e.addEventListener("click",()=>{const n=Y()==="dark"?"light":"dark";lt(n)})}function lt(e){document.documentElement.setAttribute("data-theme",e),Re(e)}function it(){const e=document.getElementById("downloads-tab"),t=document.getElementById("history-tab");if(document.getElementById("downloads-section"),document.getElementById("history-section"),!e||!t)return;const n=Fe();x(n==="history"?"history":"downloads"),e.addEventListener("click",()=>x("downloads")),t.addEventListener("click",()=>x("history"))}function x(e){const t=document.getElementById("downloads-tab"),n=document.getElementById("history-tab"),s=document.getElementById("downloads-section"),a=document.getElementById("history-section");e==="downloads"?(t.classList.add("active"),n.classList.remove("active"),s.style.display="block",a.style.display="none",j("downloads")):e==="history"&&(n.classList.add("active"),t.classList.remove("active"),a.style.display="block",s.style.display="none",j("history"),L())}function ct(){x("downloads")}document.addEventListener("DOMContentLoaded",()=>{const e=document.getElementById("login-form");e&&e.addEventListener("submit",Ze);const t=document.getElementById("logout-btn");t&&t.addEventListener("click",et);const n=document.getElementById("show-all-toggle");n&&n.addEventListener("change",i=>Ne(i.target.checked));const s=document.getElementById("status-toggle");s&&s.addEventListener("click",Je);const a=document.getElementById("home-btn");a&&a.addEventListener("click",ct),rt(),it(),at(),$e(),Pe(),ge().then(i=>{const c=document.getElementById("app-version");c&&i&&(c.textContent="v"+i)}),Ye()});