From e2a71e65a13325c76b6037782997a590d196b0f2 Mon Sep 17 00:00:00 2001 From: Gronod Date: Wed, 20 May 2026 23:45:08 +0100 Subject: [PATCH] refactor: Complete technical debt remediation (all steps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracted TagMatcher, DownloadAssembler, DownloadBuilder, and WebhookStatus services Slimmed dashboard.js from 1360 → 284 lines (pure HTTP layer) Created server/routes/status.js and mounted at /api/status Migrated frontend to vanilla ES modules under client/src/ Eliminated all tag-badge and client-logo duplication Wired Vite build into Dockerfile and removed obsolete public/app.js Added comprehensive DownloadBuilder regression tests --- Dockerfile | 13 +++++++++++++ public/app.js | 38 +++++++++++++++++++------------------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1ed08eb..96f16c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,6 +9,18 @@ WORKDIR /app COPY package.json package-lock.json ./ RUN npm ci --omit=dev +# --------------------------------------------------------------------------- +# Stage 1.5 — client-build: build frontend with Vite +# --------------------------------------------------------------------------- +FROM node:22-alpine AS client-build + +WORKDIR /app/client + +COPY client/package.json client/package-lock.json ./ +RUN npm ci +COPY client/ ./ +RUN npm run build + # --------------------------------------------------------------------------- # Stage 2 — runtime image (minimal attack surface) # --------------------------------------------------------------------------- @@ -33,6 +45,7 @@ COPY --from=deps /app/node_modules ./node_modules # Copy application source owned by root (read-only at runtime) COPY --chown=root:root server/ ./server/ COPY --chown=root:root public/ ./public/ +COPY --from=client-build --chown=root:root /app/public/app.js ./public/app.js COPY --chown=root:root package.json ./ # Bundled snakeoil certificate for out-of-the-box TLS (self-signed, localhost only). # Mount your own cert/key over /app/certs/ or set TLS_CERT/TLS_KEY env vars. diff --git a/public/app.js b/public/app.js index 8929d03..36d3c39 100644 --- a/public/app.js +++ b/public/app.js @@ -1,11 +1,11 @@ -const r={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},re=1200,le=5*60*1e3,ie=5e3;async function ce(){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&&(r.csrfToken=s.csrfToken),n.authenticated?(r.currentUser=n.user,r.isAdmin=!!n.user.isAdmin,{authenticated:!0,user:n.user}):{authenticated:!1}}catch(e){return console.error("Authentication check failed:",e),{authenticated:!1}}}async function de(e,t,n){try{const o=await(await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:t,rememberMe:n})})).json();return o.success?(r.currentUser=o.user,r.isAdmin=!!o.user.isAdmin,o.csrfToken&&(r.csrfToken=o.csrfToken),{success:!0,user:o.user}):{success:!1,error:o.error||"Login failed"}}catch(s){return console.error(s),{success:!1,error:"Login failed. Please try again."}}}async function ue(){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 me(e=!1){try{const t=new URLSearchParams({days:r.historyDays});r.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 he(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 pe(){try{return(await(await fetch("/health")).json()).version||null}catch{return null}}async function ge(){try{const e=await fetch("/api/dashboard/webhook-metrics");return e.ok?await e.json():null}catch{return null}}async function x(){var e,t;try{const n=ge();let s=!1,o={onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1};try{const a=await fetch("/api/sonarr/notifications");if(a.ok){const c=(await a.json()).find(m=>m.name==="Sofarr");s=!!c,c&&(o={onGrab:c.onGrab,onDownload:c.onDownload,onImport:c.onImport,onUpgrade:c.onUpgrade})}}catch{}let d=!1,u={onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1};try{const a=await fetch("/api/radarr/notifications");if(a.ok){const c=(await a.json()).find(m=>m.name==="Sofarr");d=!!c,c&&(u={onGrab:c.onGrab,onDownload:c.onDownload,onImport:c.onImport,onUpgrade:c.onUpgrade})}}catch{}r.webhookMetrics=await n;const h=r.webhookMetrics?Object.entries(r.webhookMetrics.instances||{}):[],l=((e=h.find(([a])=>a.includes("sonarr")))==null?void 0:e[1])||null,g=((t=h.find(([a])=>a.includes("radarr")))==null?void 0:t[1])||null;return r.sonarrWebhook={enabled:s,triggers:o,stats:l},r.radarrWebhook={enabled:d,triggers:u,stats:g},{success:!0}}catch(n){return console.error("Failed to fetch webhook status:",n),{success:!1}}}async function fe(){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 x(),{success:!0}}catch(e){return console.error("Failed to enable Sonarr webhook:",e),{success:!1,error:e.message}}}async function ye(){try{if(!(await fetch("/api/radarr/notifications/sofarr-webhook",{method:"POST",headers:{"X-CSRF-Token":r.csrfToken||""}})).ok)throw new Error("Failed to enable");return await x(),{success:!0}}catch(e){return console.error("Failed to enable Radarr webhook:",e),{success:!1,error:e.message}}}async function be(){try{const e=await fetch("/api/sonarr/notifications");if(!e.ok)throw new Error("Failed to fetch notifications");const n=(await e.json()).find(o=>o.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":r.csrfToken||""},body:JSON.stringify(n)})).ok)throw new Error("Test failed");return await x(),{success:!0}}catch(e){return console.error("Failed to test Sonarr webhook:",e),{success:!1,error:e.message}}}async function ve(){try{const e=await fetch("/api/radarr/notifications");if(!e.ok)throw new Error("Failed to fetch notifications");const n=(await e.json()).find(o=>o.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":r.csrfToken||""},body:JSON.stringify(n)})).ok)throw new Error("Test failed");return await x(),{success:!0}}catch(e){return console.error("Failed to test Radarr webhook:",e),{success:!1,error:e.message}}}async function Ee(){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 ke(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 we(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 o="S"+String(s.season).padStart(2,"0")+"E"+String(s.episode).padStart(2,"0");return s.title?o+" — "+s.title:o});t.setAttribute("data-tooltip",n.join(` -`))}return t}function K(){const e=document.getElementById("downloads-list"),t=document.getElementById("no-downloads");let n=r.downloads;if(r.selectedDownloadClients.length>0){const d=r.selectedDownloadClients.map(u=>r.downloadClients[u]).filter(Boolean);n=r.downloads.filter(u=>d.some(h=>h.type===u.client&&h.id===u.instanceId))}if(r.downloadClients.length>0){const d=new Map(r.downloadClients.map((u,h)=>[u.id,h]));n=[...n].sort((u,h)=>{const l=d.get(u.instanceId)??1/0,g=d.get(h.instanceId)??1/0;return l-g})}if(n.length===0){t.style.display="block",e.innerHTML="";return}t.style.display="none";const s=new Map;e.querySelectorAll(".download-card").forEach(d=>{s.set(d.dataset.id,d)});const o=new Set;n.forEach(d=>{const u=d.title;o.add(u);const h=s.get(u);if(h)Se(h,d);else{const l=Be(d);e.appendChild(l)}}),s.forEach((d,u)=>{o.has(u)||d.remove()})}function Se(e,t){const n=e.querySelector(".download-header-right");n&&n.remove(),e.querySelectorAll(".download-header .download-user-badge").forEach(i=>i.remove());const o=e.querySelector(".download-header .download-client-logo-wrapper");o&&o.remove();const d=e.querySelector(".download-card-logo-wrapper");d&&d.remove();const u=e.querySelector(".download-header");if(u&&!u.querySelector(".download-header-right")){const i=document.createElement("div");if(i.className="download-header-right",r.showAll&&t.tagBadges&&t.tagBadges.length>0){const c=t.tagBadges.filter(f=>!f.matchedUser),m=t.tagBadges.filter(f=>f.matchedUser);for(const f of c){const y=document.createElement("span");y.className="download-user-badge unmatched",y.textContent=f.label,i.appendChild(y)}for(const f of m){const y=document.createElement("span");y.className="download-user-badge",y.textContent=f.matchedUser,i.appendChild(y)}}else if(t.matchedUserTag){const c=document.createElement("span");c.className="download-user-badge",c.textContent=t.matchedUserTag,i.appendChild(c)}u.appendChild(i)}if(t.client&&!e.querySelector(".download-card-logo-wrapper")){const i=document.createElement("span");i.className="download-client-logo-wrapper download-card-logo-wrapper";const c=document.createElement("img");c.className="download-client-logo",c.src=`/images/clients/${t.client}.svg`,c.alt=`${t.instanceName||t.client} icon`,c.title=t.instanceName||t.client,c.onerror=()=>{i.textContent=t.client.charAt(0).toUpperCase(),i.classList.add("fallback")},i.appendChild(c),e.appendChild(i)}const h=e.querySelector(".download-status");h&&h.textContent!==t.status&&(h.textContent=t.status,h.className=`download-status ${t.status}`);const l=e.querySelector(".progress-container");if(l&&t.progress!==void 0){const i=l.querySelector(".progress-bar"),c=l.querySelector(".progress-text"),m=l.querySelector(".missing-text");if(i){const f=i.querySelector(".downloaded");f&&(f.style.width=t.progress+"%")}if(c&&(c.textContent=t.progress+"%"),m){const f=parseFloat(t.mb)||parseFloat(t.size),y=parseFloat(t.mbmissing)||0;y>0&&f>0?m.textContent=`(missing ${y.toFixed(1)} of ${f.toFixed(1)} MB)`:m.textContent=""}}const g=e.querySelector('.detail-item[data-label="Speed"] .detail-value');g&&t.speed!==void 0&&(g.textContent=t.speed);const a=e.querySelector('.detail-item[data-label="ETA"] .detail-value');if(a&&t.eta!==void 0&&(a.textContent=t.eta),t.qbittorrent){const i=e.querySelector('.detail-item[data-label="Seeds"] .detail-value');i&&t.seeds!==void 0&&(i.textContent=t.seeds);const c=e.querySelector('.detail-item[data-label="Peers"] .detail-value');c&&t.peers!==void 0&&(c.textContent=t.peers);const m=e.querySelector('.detail-item[data-label="Availability"]');m&&t.availability!==void 0&&(m.querySelector(".detail-value").textContent=`${t.availability}%`,m.classList.toggle("availability-warning",parseFloat(t.availability)<100))}}async function Ie(e,t){if(confirm(`Blocklist "${t.title}" and trigger a new search? +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 he(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 Be(e){const t=document.createElement("div");if(t.className=`download-card ${e.type}`,t.dataset.id=e.title,e.coverArt){const a=document.createElement("div");a.className="download-cover";const i=document.createElement("img");i.src=e.coverArt?"/api/dashboard/cover-art?url="+encodeURIComponent(e.coverArt):"",i.alt=e.movieName||e.seriesName||e.title,i.loading="lazy",a.appendChild(i),t.appendChild(a)}const n=document.createElement("div");n.className="download-info";const s=document.createElement("div");s.className="download-header";const o=document.createElement("span");if(o.className=`download-type ${e.type}`,e.type==="series")o.textContent="📺 Series";else if(e.type==="movie")o.textContent="🎬 Movie";else if(e.type==="torrent"){const a=e.instanceName?` (${e.instanceName})`:"";o.textContent=`📥 Torrent${a}`}else o.textContent=e.type;const d=document.createElement("span");if(d.className=`download-status ${e.status}`,d.textContent=e.status,s.appendChild(o),s.appendChild(d),e.importIssues&&e.importIssues.length>0){const a=document.createElement("span");a.className="import-issue-badge",a.textContent="Import Pending",a.setAttribute("data-tooltip",e.importIssues.join(` -`)),s.appendChild(a)}if((r.isAdmin||e.canBlocklist)&&e.arrQueueId){const a=document.createElement("button");a.className="blocklist-search-btn",a.textContent="⛔ Blocklist & Search",a.title="Remove this release from the download client, add it to the blocklist, and trigger a new automatic search",a.addEventListener("click",()=>Ie(a,e)),s.appendChild(a)}const u=document.createElement("div");if(u.className="download-header-right",r.showAll&&e.tagBadges&&e.tagBadges.length>0){const a=e.tagBadges.filter(c=>!c.matchedUser),i=e.tagBadges.filter(c=>c.matchedUser);for(const c of a){const m=document.createElement("span");m.className="download-user-badge unmatched",m.textContent=c.label,u.appendChild(m)}for(const c of i){const m=document.createElement("span");m.className="download-user-badge",m.textContent=c.matchedUser,u.appendChild(m)}}else if(e.matchedUserTag){const a=document.createElement("span");a.className="download-user-badge",a.textContent=e.matchedUserTag,u.appendChild(a)}if(s.appendChild(u),e.client){const a=document.createElement("span");a.className="download-client-logo-wrapper download-card-logo-wrapper";const i=document.createElement("img");i.className="download-client-logo",i.src=`/images/clients/${e.client}.svg`,i.alt=`${e.instanceName||e.client} icon`,i.title=e.instanceName||e.client,i.onerror=()=>{a.textContent=e.client.charAt(0).toUpperCase(),a.classList.add("fallback")},a.appendChild(i),t.appendChild(a)}const h=document.createElement("h3");if(h.className="download-title",h.textContent=e.title,n.appendChild(s),n.appendChild(h),e.seriesName){const a=document.createElement("p");a.className="download-series",r.isAdmin&&e.arrLink?a.innerHTML='Series: '+v(e.seriesName)+"":a.textContent=`Series: ${e.seriesName}`,n.appendChild(a);const i=J(e.episodes);i&&n.appendChild(i)}if(e.movieName){const a=document.createElement("p");a.className="download-movie",r.isAdmin&&e.arrLink?a.innerHTML='Movie: '+v(e.movieName)+"":a.textContent=`Movie: ${e.movieName}`,n.appendChild(a)}if(r.showAll&&e.tagBadges&&e.tagBadges.length>0){const a=e.tagBadges.filter(c=>!c.matchedUser),i=e.tagBadges.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(e.matchedUserTag){const a=document.createElement("span");a.className="download-user-badge",a.textContent=e.matchedUserTag,s.appendChild(a)}if(e.client){const a=document.createElement("span");a.className="download-client-logo-wrapper download-card-logo-wrapper";const i=document.createElement("img");i.className="download-client-logo",i.src=`/images/clients/${e.client}.svg`,i.alt=`${e.instanceName||e.client} icon`,i.title=e.instanceName||e.client,i.onerror=()=>{a.textContent=e.client.charAt(0).toUpperCase(),a.classList.add("fallback")},a.appendChild(i),s.appendChild(a)}const l=document.createElement("div");l.className="download-details";const g=S("Size",ke(e.size));if(l.appendChild(g),e.progress!==void 0){const a=document.createElement("div");a.className="detail-item progress-item",a.dataset.label="Progress";const i=document.createElement("span");i.className="detail-label",i.textContent="Progress";const c=document.createElement("div");c.className="progress-container";const m=parseFloat(e.mb)||parseFloat(e.size),f=parseFloat(e.mbmissing)||0,y=parseFloat(e.progress)||0,I=document.createElement("div");if(I.className="progress-bar",y>0){const b=document.createElement("div");b.className="progress-segment downloaded",b.style.width=y+"%",I.appendChild(b)}c.appendChild(I);const B=document.createElement("span");if(B.className="progress-text",B.textContent=e.progress+"%",c.appendChild(B),e.client&&(e.client==="qbittorrent"||e.client==="rtorrent")&&f>0&&m>0){const b=document.createElement("span");b.className="missing-text",b.textContent=`(missing ${f.toFixed(1)} of ${m.toFixed(1)} MB)`,c.appendChild(b)}a.appendChild(i),a.appendChild(c),l.appendChild(a)}if(e.speed&&e.speed>0){const a=S("Speed",we(e.speed));l.appendChild(a)}if(e.eta){const a=S("ETA",e.eta);l.appendChild(a)}if(e.qbittorrent){if(e.seeds!==void 0){const a=S("Seeds",e.seeds);l.appendChild(a)}if(e.peers!==void 0){const a=S("Peers",e.peers);l.appendChild(a)}if(e.availability!==void 0){const a=S("Availability",`${e.availability}%`);parseFloat(e.availability)<100&&a.classList.add("availability-warning"),l.appendChild(a)}}if(e.completedAt){const a=S("Completed",Te(e.completedAt));l.appendChild(a)}if(r.isAdmin&&(e.downloadPath||e.targetPath)){const a=document.createElement("div");if(a.className="download-paths",e.downloadPath){const i=document.createElement("div");i.className="path-item",i.innerHTML='Download: '+v(e.downloadPath)+"",a.appendChild(i)}if(e.targetPath){const i=document.createElement("div");i.className="path-item",i.innerHTML='Target: '+v(e.targetPath)+"",a.appendChild(i)}l.appendChild(a)}return n.appendChild(l),t.appendChild(n),t}function S(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 o=document.createElement("span");return o.className="detail-value",o.textContent=t,n.appendChild(s),n.appendChild(o),n}function Te(e){return e?new Date(e).toLocaleString():"N/A"}function M(){X();const e=r.showAll?"?showAll=true":"",t=new EventSource("/api/dashboard/stream"+e);r.sseSource=t;let n=!0;t.onmessage=s=>{try{const o=JSON.parse(s.data);if(r.currentUser=o.user,r.isAdmin=!!o.isAdmin,r.downloads=o.downloads,o.downloadClients){r.downloadClients=o.downloadClients;const d=new CustomEvent("downloadClientsUpdated");document.dispatchEvent(d)}document.getElementById("currentUser").textContent=r.currentUser||"-",K(),et(),n&&(n=!1,tt())}catch(o){console.error("[SSE] Failed to parse message:",o)}},t.onerror=()=>{console.warn("[SSE] Connection lost, browser will retry...")},console.log("[SSE] Stream connected")}function X(){r.sseSource&&(r.sseSource.close(),r.sseSource=null,console.log("[SSE] Stream closed"))}function Ne(e){r.showAll=e,M();const t=new CustomEvent("historyReload");document.dispatchEvent(t)}(function(){const t=localStorage.getItem("sofarr-download-client");if(t&&t!=="all")try{r.selectedDownloadClients=[t],localStorage.setItem("sofarr-download-clients",JSON.stringify(r.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");r.selectedDownloadClients=n?JSON.parse(n):[]}catch(n){console.error("[Migration] Failed to load download client filter:",n),r.selectedDownloadClients=[]}})();(function(){try{const t=localStorage.getItem("sofarr-history-days");t&&(r.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");r.ignoreAvailable=t==="true"}catch(t){console.error("[Storage] Failed to load ignore available:",t)}})();function xe(e){localStorage.setItem("sofarr-history-days",e)}function Le(e){localStorage.setItem("sofarr-ignore-available",e)}function De(e){localStorage.setItem("sofarr-download-clients",JSON.stringify(e))}function Q(){return localStorage.getItem("sofarr-theme")||"light"}function Ae(e){localStorage.setItem("sofarr-theme",e)}function Me(){return localStorage.getItem("sofarr-active-tab")||"downloads"}function O(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,xe(s),N(!0))}),t&&t.addEventListener("click",()=>N(!0)),n&&(n.checked=ignoreAvailable,n.addEventListener("change",()=>{ignoreAvailable=n.checked,Le(ignoreAvailable),Y(lastHistoryItems)})),document.addEventListener("historyReload",()=>{N(!0)})}function Re(){V(),r.historyRefreshHandle=setInterval(()=>N(),le)}function V(){r.historyRefreshHandle&&(clearInterval(r.historyRefreshHandle),r.historyRefreshHandle=null)}function Fe(){r.lastHistoryItems=[],document.getElementById("history-list").innerHTML="",document.getElementById("no-history").style.display="none",document.getElementById("history-error").style.display="none"}async function N(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 o=await me(e);t.style.display="none",o.success?(r.lastHistoryItems=o.history,Y(r.lastHistoryItems)):(n.textContent=o.error||"Failed to load history.",n.style.display="block")}catch(o){t.style.display="none",n.textContent="Failed to load history.",n.style.display="block",console.error("[History] Load error:",o)}}function Y(e){const t=document.getElementById("history-list"),n=document.getElementById("no-history");t.innerHTML="";const s=r.ignoreAvailable?e.filter(o=>!(o.outcome==="failed"&&o.availableForUpgrade)):e;if(!s.length){n.style.display="block";return}n.style.display="none",s.forEach(o=>t.appendChild(He(o)))}function He(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 g=document.createElement("img");g.src="/api/dashboard/cover-art?url="+encodeURIComponent(e.coverArt),g.alt=e.movieName||e.seriesName||e.title,g.loading="lazy",l.appendChild(g),t.appendChild(l)}const n=document.createElement("div");n.className="history-info";const s=document.createElement("div");s.className="history-card-header";const o=document.createElement("span");o.className=`history-type-badge ${e.type}`,o.textContent=e.type==="series"?"📺 Series":"🎬 Movie",s.appendChild(o);const d=document.createElement("span");if(d.className=`history-outcome-badge ${e.outcome}`,d.textContent=e.outcome==="imported"?"✓ Imported":"✗ Failed",s.appendChild(d),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)}if(r.showAll&&e.tagBadges&&e.tagBadges.length>0){const l=e.tagBadges.filter(a=>!a.matchedUser),g=e.tagBadges.filter(a=>a.matchedUser);for(const a of l){const i=document.createElement("span");i.className="download-user-badge unmatched",i.textContent=a.label,s.appendChild(i)}for(const a of g){const i=document.createElement("span");i.className="download-user-badge",i.textContent=a.matchedUser,s.appendChild(i)}}else if(e.matchedUserTag){const l=document.createElement("span");l.className="download-user-badge",l.textContent=e.matchedUserTag,s.appendChild(l)}n.appendChild(s);const u=document.createElement("h3");if(u.className="history-title",u.textContent=e.title,n.appendChild(u),e.seriesName){const l=document.createElement("p");l.className="history-media-name",r.isAdmin&&e.arrLink?l.innerHTML='Series: '+v(e.seriesName)+"":l.textContent="Series: "+e.seriesName,n.appendChild(l);const g=J(e.episodes);g&&n.appendChild(g)}if(e.movieName){const l=document.createElement("p");l.className="history-media-name",r.isAdmin&&e.arrLink?l.innerHTML='Movie: '+v(e.movieName)+"":l.textContent="Movie: "+e.movieName,n.appendChild(l)}const h=document.createElement("div");if(h.className="history-details",e.completedAt&&h.appendChild(j("Completed",Ce(e.completedAt))),e.quality&&h.appendChild(j("Quality",e.quality)),e.outcome==="failed"&&e.failureMessage){const l=document.createElement("div");l.className="history-failure-message",l.textContent=e.failureMessage,h.appendChild(l)}return n.appendChild(h),t.appendChild(n),t}function j(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 o=document.createElement("span");return o.className="detail-value",o.textContent=t,n.appendChild(s),n.appendChild(o),n}function Ue(){document.getElementById("webhooks-section")&&(document.getElementById("webhooks-header").addEventListener("click",We),document.getElementById("enable-sonarr-webhook").addEventListener("click",qe),document.getElementById("enable-radarr-webhook").addEventListener("click",Oe),document.getElementById("test-sonarr-webhook").addEventListener("click",je),document.getElementById("test-radarr-webhook").addEventListener("click",Ge))}function We(){r.webhookSectionExpanded=!r.webhookSectionExpanded;const e=document.getElementById("webhooks-content"),t=document.getElementById("webhooks-toggle");e.style.display=r.webhookSectionExpanded?"":"none",t.classList.toggle("expanded",r.webhookSectionExpanded),r.webhookSectionExpanded&&Z()}async function Z(){const e=document.getElementById("webhook-loading");e.style.display="";try{(await x()).success&&Pe()}catch(t){console.error("Failed to fetch webhook status:",t)}finally{e.style.display="none"}}function Pe(){const e=document.getElementById("sonarr-status"),t=document.getElementById("enable-sonarr-webhook"),n=document.getElementById("test-sonarr-webhook"),s=document.getElementById("sonarr-triggers"),o=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?(o.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=q(sonarrWebhook.stats.lastWebhookTimestamp)):o.style.display="none";const d=document.getElementById("radarr-status"),u=document.getElementById("enable-radarr-webhook"),h=document.getElementById("test-radarr-webhook"),l=document.getElementById("radarr-triggers"),g=document.getElementById("radarr-stats");d.textContent=radarrWebhook.enabled?"● Enabled":"○ Disabled",d.className="status-indicator "+(radarrWebhook.enabled?"enabled":"disabled"),u.style.display=radarrWebhook.enabled?"none":"",h.style.display=radarrWebhook.enabled?"":"none",l.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?(g.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=q(radarrWebhook.stats.lastWebhookTimestamp)):g.style.display="none"}async function qe(){k(!0);try{const e=await fe();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 Oe(){k(!0);try{const e=await ye();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 je(){k(!0);try{const e=await be();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 Ge(){k(!0);try{const e=await ve();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){r.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 _e(){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"),r.statusRefreshHandle&&(clearInterval(r.statusRefreshHandle),r.statusRefreshHandle=null);return}e.style.display="block",t&&r.isAdmin?(t.style.display="block",r.webhookSectionExpanded=!1,document.getElementById("webhooks-content").style.display="none",document.getElementById("webhooks-toggle").classList.remove("expanded"),await Z()):t&&(t.style.display="none"),G(),r.statusRefreshHandle&&clearInterval(r.statusRefreshHandle),r.statusRefreshHandle=setInterval(G,ie)}function ze(){document.getElementById("status-panel").style.display="none";const e=document.getElementById("webhooks-section");e&&(e.style.display="none"),r.statusRefreshHandle&&(clearInterval(r.statusRefreshHandle),r.statusRefreshHandle=null)}async function G(){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 Ee();s.success&&(console.log("[Status] Got status data, rendering..."),Je(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 Je(e,t){var B,b,$,R,F,H,U,W,P;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),o=Math.floor(n.uptimeSeconds%3600/60),d=n.uptimeSeconds%60,u=`${s}h ${o}m ${d}s`,h=(e.cache.totalSizeBytes/1024).toFixed(1);let l=` +• 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

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