d3d085d614
Build and Push Docker Image / build (push) Successful in 1m29s
Docs Check / Markdown lint (push) Successful in 1m51s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m3s
CI / Security audit (push) Successful in 2m54s
CI / Swagger Validation & Coverage (push) Successful in 3m6s
Docs Check / Mermaid diagram parse check (push) Successful in 3m13s
CI / Tests & coverage (push) Successful in 3m31s
- Add request filters UI (type, status, sort, search) - Implement dual-layer filtering (server + client) - Add ombiFilters utility for consistent filtering logic - Persist filter preferences in localStorage - Add SSE support for real-time Ombi request updates - Add webhook endpoints for Ombi integration - Update OpenAPI spec for new endpoints - Add unit tests for filter logic and UI - Add integration tests for Ombi routes
43 lines
57 KiB
JavaScript
43 lines
57 KiB
JavaScript
const n={currentUser:null,downloads:[],downloadClients:[],selectedDownloadClients:[],isAdmin:!1,showAll:!1,csrfToken:null,ombiBaseUrl:null,ombiRequests: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},ombiWebhook:{enabled:!1,triggers:{requestAvailable:!1,requestApproved:!1,requestDeclined:!1,requestPending:!1,requestProcessing:!1},stats:null},webhookMetrics:null,selectedRequestTypes:["movie","tv"],selectedRequestStatuses:[],requestSortMode:"requestedDate_desc",requestSearchQuery:""},ve=1200,Ee=5*60*1e3,ke=5e3;async function Se(){try{const[e,t]=await Promise.all([fetch("/api/auth/me"),fetch("/api/auth/csrf")]),s=await e.json(),o=await t.json();return o.csrfToken&&(n.csrfToken=o.csrfToken),s.authenticated?(n.currentUser=s.user,n.isAdmin=!!s.user.isAdmin,{authenticated:!0,user:s.user}):{authenticated:!1}}catch(e){return console.error("Authentication check failed:",e),{authenticated:!1}}}async function we(e,t,s){try{const a=await(await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:e,password:t,rememberMe:s})})).json();return a.success?(n.currentUser=a.user,n.isAdmin=!!a.user.isAdmin,a.csrfToken&&(n.csrfToken=a.csrfToken),{success:!0,user:a.user}):{success:!1,error:a.error||"Login failed"}}catch(o){return console.error(o),{success:!1,error:"Login failed. Please try again."}}}async function Ce(){try{return await fetch("/api/auth/logout",{method:"POST",headers:n.csrfToken?{"X-CSRF-Token":n.csrfToken}:{}}),n.currentUser=null,n.csrfToken=null,{success:!0}}catch(e){return console.error("Logout failed:",e),{success:!1}}}async function Ie(e=!1){try{const t=new URLSearchParams({days:n.historyDays});n.showAll&&t.set("showAll","true"),e&&t.set("_t",Date.now());const s=await fetch(`/api/history/recent?${t}`);if(!s.ok)throw new Error(`HTTP ${s.status}`);return{success:!0,history:(await s.json()).history||[]}}catch(t){return console.error("[History] Load error:",t),{success:!1,error:"Failed to load history."}}}async function Be(e){try{const t=await fetch("/api/dashboard/blocklist-search",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":n.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 s=await t.json().catch(()=>({}));throw new Error(s.error||`HTTP ${t.status}`)}return{success:!0}}catch(t){throw console.error("[Blocklist] Error:",t),t}}async function Le(){try{return(await(await fetch("/health")).json()).version||null}catch{return null}}async function qe(){try{const e=await fetch("/api/dashboard/webhook-metrics");return e.ok?await e.json():null}catch{return null}}async function L(){var e,t;try{const s=qe();let o=!1;try{const h=await fetch("/api/webhook/config");h.ok&&(o=(await h.json()).valid||!1)}catch{}let a=!1,r={onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1};try{const h=await fetch("/api/sonarr/notifications");if(h.ok){const b=(await h.json()).find(E=>E.name==="Sofarr");a=o&&!!b,b&&(r={onGrab:b.onGrab,onDownload:b.onDownload,onImport:b.onImport,onUpgrade:b.onUpgrade})}}catch{}let c=!1,m={onGrab:!1,onDownload:!1,onImport:!1,onUpgrade:!1};try{const h=await fetch("/api/radarr/notifications");if(h.ok){const b=(await h.json()).find(E=>E.name==="Sofarr");c=o&&!!b,b&&(m={onGrab:b.onGrab,onDownload:b.onDownload,onImport:b.onImport,onUpgrade:b.onUpgrade})}}catch{}let d=!1,l={requestAvailable:!1,requestApproved:!1,requestDeclined:!1,requestPending:!1,requestProcessing:!1},f=null;try{const h=await fetch("/api/ombi/webhook/status");if(h.ok){const y=await h.json();d=y.enabled||!1,l=y.triggers||{requestAvailable:!1,requestApproved:!1,requestDeclined:!1,requestPending:!1,requestProcessing:!1},f=y.stats||null}}catch{}n.webhookMetrics=await s;const i=n.webhookMetrics?Object.entries(n.webhookMetrics.instances||{}):[],u=((e=i.find(([h])=>h.includes("sonarr")))==null?void 0:e[1])||null,p=((t=i.find(([h])=>h.includes("radarr")))==null?void 0:t[1])||null;return n.sonarrWebhook={enabled:a,triggers:r,stats:u},n.radarrWebhook={enabled:c,triggers:m,stats:p},n.ombiWebhook={enabled:d,triggers:l,stats:f},{success:!0}}catch(s){return console.error("Failed to fetch webhook status:",s),{success:!1}}}async function Te(){try{if(!(await fetch("/api/sonarr/notifications/sofarr-webhook",{method:"POST",headers:{"X-CSRF-Token":n.csrfToken||""}})).ok)throw new Error("Failed to enable");return await L(),{success:!0}}catch(e){return console.error("Failed to enable Sonarr webhook:",e),{success:!1,error:e.message}}}async function xe(){try{if(!(await fetch("/api/radarr/notifications/sofarr-webhook",{method:"POST",headers:{"X-CSRF-Token":n.csrfToken||""}})).ok)throw new Error("Failed to enable");return await L(),{success:!0}}catch(e){return console.error("Failed to enable Radarr webhook:",e),{success:!1,error:e.message}}}async function Ne(){try{const e=await fetch("/api/sonarr/notifications");if(!e.ok)throw new Error("Failed to fetch notifications");const s=(await e.json()).find(a=>a.name==="Sofarr");if(!s)throw new Error("Sofarr webhook not found");if(!(await fetch("/api/sonarr/notifications/test",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":n.csrfToken||""},body:JSON.stringify(s)})).ok)throw new Error("Test failed");return await L(),{success:!0}}catch(e){return console.error("Failed to test Sonarr webhook:",e),{success:!1,error:e.message}}}async function Re(){try{const e=await fetch("/api/radarr/notifications");if(!e.ok)throw new Error("Failed to fetch notifications");const s=(await e.json()).find(a=>a.name==="Sofarr");if(!s)throw new Error("Sofarr webhook not found");if(!(await fetch("/api/radarr/notifications/test",{method:"POST",headers:{"Content-Type":"application/json","X-CSRF-Token":n.csrfToken||""},body:JSON.stringify(s)})).ok)throw new Error("Test failed");return await L(),{success:!0}}catch(e){return console.error("Failed to test Radarr webhook:",e),{success:!1,error:e.message}}}async function De(){try{if(!(await fetch("/api/ombi/webhook/enable",{method:"POST",headers:{"X-CSRF-Token":n.csrfToken||""}})).ok)throw new Error("Failed to enable");return await L(),{success:!0}}catch(e){return console.error("Failed to enable Ombi webhook:",e),{success:!1,error:e.message}}}async function Ae(){try{if(!(await fetch("/api/ombi/webhook/test",{method:"POST",headers:{"X-CSRF-Token":n.csrfToken||""}})).ok)throw new Error("Test failed");return await L(),{success:!0}}catch(e){return console.error("Failed to test Ombi webhook:",e),{success:!1,error:e.message}}}async function Fe(){try{const e=await fetch("/api/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 Me(e){if(!e)return"N/A";if(typeof e=="string")return e;const t=["B","KB","MB","GB","TB"],s=Math.floor(Math.log(e)/Math.log(1024));return Math.round(e/Math.pow(1024,s)*100)/100+" "+t[s]}function te(e){if(!e||e===0)return"0 B/s";const t=["B/s","KB/s","MB/s","GB/s"];let s=e,o=0;for(;s>=1024&&o<t.length-1;)s/=1024,o++;return`${s.toFixed(2)} ${t[o]}`}function $e(e){return e?new Date(e).toLocaleString():"N/A"}function D(e){if(!e)return"Never";const t=Math.floor((Date.now()-e)/1e3);if(t<60)return t+"s ago";const s=Math.floor(t/60);if(s<60)return s+"m ago";const o=Math.floor(s/60);return o<24?o+"h ago":Math.floor(o/24)+"d ago"}function z(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}function se(e){if(!e||e.length===0)return null;const t=document.createElement("p");if(t.className="episode-info",e.length===1){const s=e[0],o="S"+String(s.season).padStart(2,"0")+"E"+String(s.episode).padStart(2,"0");t.textContent=s.title?o+" — "+s.title:o}else{t.textContent="Multiple episodes",t.classList.add("multi-episode");const s=e.map(o=>{const a="S"+String(o.season).padStart(2,"0")+"E"+String(o.episode).padStart(2,"0");return o.title?a+" — "+o.title:a});t.setAttribute("data-tooltip",s.join(`
|
|
`))}return t}function M(e,t,s){const o=document.createDocumentFragment();if(t&&e&&e.length>0){const a=e.filter(c=>!c.matchedUser),r=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,o.appendChild(m)}for(const c of r){const m=document.createElement("span");m.className="download-user-badge",m.textContent=c.matchedUser,o.appendChild(m)}}else if(s){const a=document.createElement("span");a.className="download-user-badge",a.textContent=s,o.appendChild(a)}return o}function ne(e){const t=document.createElement("span");t.className="download-client-logo-wrapper download-card-logo-wrapper";const s=document.createElement("img");return s.className="download-client-logo",s.src=`/images/clients/${e.client}.svg`,s.alt=`${e.instanceName||e.client} icon`,s.title=e.instanceName||e.client,s.onerror=()=>{t.textContent=e.client.charAt(0).toUpperCase(),t.classList.add("fallback")},t.appendChild(s),t}function J(e){const t=document.createElement("span");if(t.className="service-icons-container",e.ombiLink){const s=document.createElement("img");s.className="service-icon ombi",s.src="/images/ombi.svg",s.alt="Ombi",s.title=e.ombiTooltip||"Ombi",s.href=e.ombiLink;const o=document.createElement("a");o.href=e.ombiLink,o.target="_blank",o.appendChild(s),t.appendChild(o)}if(n.isAdmin&&e.arrLink){const s=document.createElement("img");e.arrType==="sonarr"?(s.className="service-icon sonarr",s.src="/images/sonarr.svg",s.alt="Sonarr"):e.arrType==="radarr"&&(s.className="service-icon radarr",s.src="/images/radarr.svg",s.alt="Radarr"),s.title=e.arrType==="sonarr"?"Sonarr":"Radarr";const o=document.createElement("a");o.href=e.arrLink,o.target="_blank",o.appendChild(s),t.appendChild(o)}return t}function oe(){const e=document.getElementById("downloads-list"),t=document.getElementById("no-downloads");let s=n.downloads;if(n.selectedDownloadClients.length>0){const r=n.selectedDownloadClients.map(c=>n.downloadClients[c]).filter(Boolean);s=n.downloads.filter(c=>r.some(m=>m.type===c.client&&m.id===c.instanceId))}if(n.downloadClients.length>0){const r=new Map(n.downloadClients.map((c,m)=>[c.id,m]));s=[...s].sort((c,m)=>{const d=r.get(c.instanceId)??1/0,l=r.get(m.instanceId)??1/0;return d-l})}if(s.length===0){t.classList.remove("hidden"),e.innerHTML="";return}t.classList.add("hidden");const o=new Map;e.querySelectorAll(".download-card").forEach(r=>{o.set(r.dataset.id,r)});const a=new Set;s.forEach(r=>{const c=r.title;a.add(c);const m=o.get(c);if(m)We(m,r);else{const d=Ue(r);e.appendChild(d)}}),o.forEach((r,c)=>{a.has(c)||r.remove()})}function We(e,t){const s=e.querySelector(".download-header-right");s&&s.remove(),e.querySelectorAll(".download-header .download-user-badge").forEach(i=>i.remove());const a=e.querySelector(".download-header .download-client-logo-wrapper");a&&a.remove();const r=e.querySelector(".download-card-logo-wrapper");r&&r.remove();const c=e.querySelector(".download-header");if(c&&!c.querySelector(".download-header-right")){const i=document.createElement("div");i.className="download-header-right";const u=M(t.tagBadges,n.showAll,t.matchedUserTag);i.appendChild(u),c.appendChild(i)}t.client&&!e.querySelector(".download-card-logo-wrapper")&&e.appendChild(ne(t));const m=e.querySelector(".download-status");m&&m.textContent!==t.status&&(m.textContent=t.status,m.className=`download-status ${t.status}`);const d=e.querySelector(".progress-container");if(d&&t.progress!==void 0){const i=d.querySelector(".progress-bar"),u=d.querySelector(".progress-text"),p=d.querySelector(".missing-text");if(i){const h=i.querySelector(".downloaded");h&&(h.style.width=t.progress+"%")}if(u&&(u.textContent=t.progress+"%"),p){const h=parseFloat(t.mb)||parseFloat(t.size),y=parseFloat(t.mbmissing)||0;y>0&&h>0?p.textContent=`(missing ${y.toFixed(1)} of ${h.toFixed(1)} MB)`:p.textContent=""}}const l=e.querySelector('.detail-item[data-label="Speed"] .detail-value');l&&t.speed!==void 0&&(l.textContent=te(t.speed));const f=e.querySelector('.detail-item[data-label="ETA"] .detail-value');if(f&&t.eta!==void 0&&(f.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 u=e.querySelector('.detail-item[data-label="Peers"] .detail-value');u&&t.peers!==void 0&&(u.textContent=t.peers);const p=e.querySelector('.detail-item[data-label="Availability"]');p&&t.availability!==void 0&&(p.querySelector(".detail-value").textContent=`${t.availability}%`,p.classList.toggle("availability-warning",parseFloat(t.availability)<100))}}async function He(e,t){if(console.log("[Blocklist] Clicked, download:",t),console.log("[Blocklist] Required fields:",{arrQueueId:t.arrQueueId,arrType:t.arrType,arrInstanceUrl:t.arrInstanceUrl,arrInstanceKey:t.arrInstanceKey,arrContentId:t.arrContentId,arrContentType:t.arrContentType,isAdmin:n.isAdmin,canBlocklist:t.canBlocklist}),!!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 Be(t),e.textContent="✓ Done — searching…",e.className="blocklist-search-btn success"}catch(s){console.error("[Blocklist] Error:",s),e.disabled=!1,e.textContent="⛔ Blocklist & Search",e.className="blocklist-search-btn error",e.title=`Failed: ${s.message}`,setTimeout(()=>{e.className="blocklist-search-btn",e.title="Remove this release from the download client, add it to the blocklist, and trigger an automatic search"},4e3)}}}function Ue(e){const t=document.createElement("div");if(t.className=`download-card ${e.type}`,t.dataset.id=e.title,e.coverArt){const i=document.createElement("div");i.className="download-cover";const u=document.createElement("img");u.src=e.coverArt?"/api/dashboard/cover-art?url="+encodeURIComponent(e.coverArt):"",u.alt=e.movieName||e.seriesName||e.title,u.loading="lazy",i.appendChild(u),t.appendChild(i)}const s=document.createElement("div");s.className="download-info";const o=document.createElement("div");o.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 i=e.instanceName?` (${e.instanceName})`:"";a.textContent=`📥 Torrent${i}`}else a.textContent=e.type;const r=document.createElement("span");if(r.className=`download-status ${e.status}`,r.textContent=e.status,o.appendChild(a),o.appendChild(r),e.importIssues&&e.importIssues.length>0){const i=document.createElement("span");i.className="import-issue-badge",i.textContent="Import Pending",i.setAttribute("data-tooltip",e.importIssues.join(`
|
|
`)),o.appendChild(i)}if((n.isAdmin||e.canBlocklist)&&e.arrQueueId){const i=document.createElement("button");i.className="blocklist-search-btn",i.textContent="⛔ Blocklist & Search",i.title="Remove this release from the download client, add it to the blocklist, and trigger a new automatic search",i.addEventListener("click",()=>He(i,e)),o.appendChild(i)}const c=document.createElement("div");c.className="download-header-right";const m=M(e.tagBadges,n.showAll,e.matchedUserTag);c.appendChild(m),o.appendChild(c),e.client&&t.appendChild(ne(e));const d=document.createElement("h3");if(d.className="download-title",d.textContent=e.title,s.appendChild(o),s.appendChild(d),e.seriesName){const i=document.createElement("p");i.className="download-series";const u=J(e);u.hasChildNodes()&&(i.appendChild(u),i.appendChild(document.createTextNode(" ")));const p=document.createElement("span");p.textContent=`Series: ${e.seriesName}`,i.appendChild(p),s.appendChild(i);const h=se(e.episodes);h&&s.appendChild(h)}if(e.movieName){const i=document.createElement("p");i.className="download-movie";const u=J(e);u.hasChildNodes()&&(i.appendChild(u),i.appendChild(document.createTextNode(" ")));const p=document.createElement("span");p.textContent=`Movie: ${e.movieName}`,i.appendChild(p),s.appendChild(i)}const l=document.createElement("div");l.className="download-details";const f=I("Size",Me(e.size));if(l.appendChild(f),e.progress!==void 0){const i=document.createElement("div");i.className="detail-item progress-item",i.dataset.label="Progress";const u=document.createElement("span");u.className="detail-label",u.textContent="Progress";const p=document.createElement("div");p.className="progress-container";const h=parseFloat(e.mb)||parseFloat(e.size),y=parseFloat(e.mbmissing)||0,b=parseFloat(e.progress)||0,E=document.createElement("div");if(E.className="progress-bar",b>0){const k=document.createElement("div");k.className="progress-segment downloaded",k.style.width=b+"%",E.appendChild(k)}p.appendChild(E);const T=document.createElement("span");if(T.className="progress-text",T.textContent=e.progress+"%",p.appendChild(T),e.client&&(e.client==="qbittorrent"||e.client==="rtorrent")&&y>0&&h>0){const k=document.createElement("span");k.className="missing-text",k.textContent=`(missing ${y.toFixed(1)} of ${h.toFixed(1)} MB)`,p.appendChild(k)}i.appendChild(u),i.appendChild(p),l.appendChild(i)}if(e.speed&&e.speed>0){const i=I("Speed",te(e.speed));l.appendChild(i)}if(e.eta){const i=I("ETA",e.eta);l.appendChild(i)}if(e.qbittorrent){if(e.seeds!==void 0){const i=I("Seeds",e.seeds);l.appendChild(i)}if(e.peers!==void 0){const i=I("Peers",e.peers);l.appendChild(i)}if(e.availability!==void 0){const i=I("Availability",`${e.availability}%`);parseFloat(e.availability)<100&&i.classList.add("availability-warning"),l.appendChild(i)}}if(e.completedAt){const i=I("Completed",Pe(e.completedAt));l.appendChild(i)}if(n.isAdmin&&(e.downloadPath||e.targetPath)){const i=document.createElement("div");if(i.className="download-paths",e.downloadPath){const u=document.createElement("div");u.className="path-item",u.innerHTML='<span class="path-label">Download:</span> <span class="path-value">'+z(e.downloadPath)+"</span>",i.appendChild(u)}if(e.targetPath){const u=document.createElement("div");u.className="path-item",u.innerHTML='<span class="path-label">Target:</span> <span class="path-value">'+z(e.targetPath)+"</span>",i.appendChild(u)}l.appendChild(i)}return s.appendChild(l),t.appendChild(s),t}function I(e,t){const s=document.createElement("div");s.className="detail-item",s.dataset.label=e;const o=document.createElement("span");o.className="detail-label",o.textContent=e;const a=document.createElement("span");return a.className="detail-value",a.textContent=t,s.appendChild(o),s.appendChild(a),s}function Pe(e){return e?new Date(e).toLocaleString():"N/A"}(function(){const t=localStorage.getItem("sofarr-download-client");if(t&&t!=="all")try{n.selectedDownloadClients=[t],localStorage.setItem("sofarr-download-clients",JSON.stringify(n.selectedDownloadClients)),localStorage.removeItem("sofarr-download-client")}catch(s){console.error("[Migration] Failed to migrate download client filter:",s)}else try{const s=localStorage.getItem("sofarr-download-clients");n.selectedDownloadClients=s?JSON.parse(s):[]}catch(s){console.error("[Migration] Failed to load download client filter:",s),n.selectedDownloadClients=[]}})();(function(){try{const t=localStorage.getItem("sofarr-history-days");t&&(n.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");n.ignoreAvailable=t==="true"}catch(t){console.error("[Storage] Failed to load ignore available:",t)}})();(function(){try{const t=localStorage.getItem("sofarr-request-types");t&&(n.selectedRequestTypes=JSON.parse(t))}catch(t){console.error("[Storage] Failed to load request types:",t),n.selectedRequestTypes=["movie","tv"]}try{const t=localStorage.getItem("sofarr-request-statuses");t&&(n.selectedRequestStatuses=JSON.parse(t))}catch(t){console.error("[Storage] Failed to load request statuses:",t),n.selectedRequestStatuses=[]}try{const t=localStorage.getItem("sofarr-request-sort");t&&(n.requestSortMode=t)}catch(t){console.error("[Storage] Failed to load request sort:",t),n.requestSortMode="requestedDate_desc"}try{const t=localStorage.getItem("sofarr-request-search");t!==null&&(n.requestSearchQuery=t)}catch(t){console.error("[Storage] Failed to load request search:",t),n.requestSearchQuery=""}})();function Oe(e){localStorage.setItem("sofarr-history-days",e)}function je(e){localStorage.setItem("sofarr-ignore-available",e)}function _e(e){localStorage.setItem("sofarr-download-clients",JSON.stringify(e))}function ae(){return localStorage.getItem("sofarr-theme")||"light"}function Ge(e){localStorage.setItem("sofarr-theme",e)}function ze(){return localStorage.getItem("sofarr-active-tab")||"downloads"}function A(e){localStorage.setItem("sofarr-active-tab",e)}function re(e){localStorage.setItem("sofarr-request-types",JSON.stringify(e))}function ie(e){localStorage.setItem("sofarr-request-statuses",JSON.stringify(e))}function Je(e){localStorage.setItem("sofarr-request-sort",e)}function Qe(e){localStorage.setItem("sofarr-request-search",e)}function Q(e){const t=document.createElement("span");if(t.className="service-icons-container",e.ombiLink){const s=document.createElement("img");s.className="service-icon ombi",s.src="/images/ombi.svg",s.alt="Ombi",s.title=e.ombiTooltip||"Ombi";const o=document.createElement("a");o.href=e.ombiLink,o.target="_blank",o.appendChild(s),t.appendChild(o)}if(n.isAdmin&&e.arrLink){const s=document.createElement("img");e.arrType==="sonarr"?(s.className="service-icon sonarr",s.src="/images/sonarr.svg",s.alt="Sonarr"):e.arrType==="radarr"&&(s.className="service-icon radarr",s.src="/images/radarr.svg",s.alt="Radarr"),s.title=e.arrType==="sonarr"?"Sonarr":"Radarr";const o=document.createElement("a");o.href=e.arrLink,o.target="_blank",o.appendChild(s),t.appendChild(o)}return t}function Ke(){const e=document.getElementById("history-days"),t=document.getElementById("history-refresh-btn"),s=document.getElementById("ignore-available-toggle");e&&e.addEventListener("change",()=>{const o=parseInt(e.value,10);o>0&&o<=90&&(historyDays=o,Oe(o),x(!0))}),t&&t.addEventListener("click",()=>x(!0)),s&&(s.checked=n.ignoreAvailable,s.addEventListener("change",()=>{n.ignoreAvailable=s.checked,je(n.ignoreAvailable),ce(n.lastHistoryItems)})),document.addEventListener("historyReload",()=>{x(!0)})}function Xe(){le(),n.historyRefreshHandle=setInterval(()=>x(),Ee)}function le(){n.historyRefreshHandle&&(clearInterval(n.historyRefreshHandle),n.historyRefreshHandle=null)}function Ve(){n.lastHistoryItems=[],document.getElementById("history-list").innerHTML="",document.getElementById("no-history").classList.add("hidden"),document.getElementById("history-error").classList.add("hidden")}async function x(e=!1){document.getElementById("history-list");const t=document.getElementById("history-loading"),s=document.getElementById("history-error"),o=document.getElementById("no-history");t.classList.remove("hidden"),s.classList.add("hidden"),o.classList.add("hidden");try{const a=await Ie(e);t.classList.add("hidden"),a.success?(n.lastHistoryItems=a.history,ce(n.lastHistoryItems)):(s.textContent=a.error||"Failed to load history.",s.classList.remove("hidden"))}catch(a){t.classList.add("hidden"),s.textContent="Failed to load history.",s.classList.remove("hidden"),console.error("[History] Load error:",a)}}function ce(e){const t=document.getElementById("history-list"),s=document.getElementById("no-history");t.innerHTML="";const o=n.ignoreAvailable?e.filter(a=>!(a.outcome==="failed"&&a.availableForUpgrade)):e;if(!o.length){s.classList.remove("hidden");return}s.classList.add("hidden"),o.forEach(a=>t.appendChild(Ye(a)))}function Ye(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 f=document.createElement("img");f.src="/api/dashboard/cover-art?url="+encodeURIComponent(e.coverArt),f.alt=e.movieName||e.seriesName||e.title,f.loading="lazy",l.appendChild(f),t.appendChild(l)}const s=document.createElement("div");s.className="history-info";const o=document.createElement("div");o.className="history-card-header";const a=document.createElement("span");a.className=`history-type-badge ${e.type}`,a.textContent=e.type==="series"?"📺 Series":"🎬 Movie",o.appendChild(a);const r=document.createElement("span");if(r.className=`history-outcome-badge ${e.outcome}`,r.textContent=e.outcome==="imported"?"✓ Imported":"✗ Failed",o.appendChild(r),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",o.appendChild(l)}if(e.instanceName){const l=document.createElement("span");l.className="history-instance-badge",l.textContent=e.instanceName,o.appendChild(l)}const c=M(e.tagBadges,n.showAll,e.matchedUserTag);o.appendChild(c),s.appendChild(o);const m=document.createElement("h3");if(m.className="history-title",m.textContent=e.title,s.appendChild(m),e.seriesName){const l=document.createElement("p");l.className="history-media-name";const f=Q(e);f.hasChildNodes()&&(l.appendChild(f),l.appendChild(document.createTextNode(" ")));const i=document.createElement("span");i.textContent="Series: "+e.seriesName,l.appendChild(i),s.appendChild(l);const u=se(e.episodes);u&&s.appendChild(u)}if(e.movieName){const l=document.createElement("p");l.className="history-media-name";const f=Q(e);f.hasChildNodes()&&(l.appendChild(f),l.appendChild(document.createTextNode(" ")));const i=document.createElement("span");i.textContent="Movie: "+e.movieName,l.appendChild(i),s.appendChild(l)}const d=document.createElement("div");if(d.className="history-details",e.completedAt&&d.appendChild(K("Completed",$e(e.completedAt))),e.quality&&d.appendChild(K("Quality",e.quality)),e.outcome==="failed"&&e.failureMessage){const l=document.createElement("div");l.className="history-failure-message",l.textContent=e.failureMessage,d.appendChild(l)}return s.appendChild(d),t.appendChild(s),t}function K(e,t){const s=document.createElement("div");s.className="detail-item",s.dataset.label=e;const o=document.createElement("span");o.className="detail-label",o.textContent=e;const a=document.createElement("span");return a.className="detail-value",a.textContent=t,s.appendChild(o),s.appendChild(a),s}function $(){de();const e=n.showAll?"?showAll=true":"",t=new EventSource("/api/dashboard/stream"+e);n.sseSource=t;let s=!0;t.onmessage=o=>{try{const a=JSON.parse(o.data);if(n.currentUser=a.user,n.isAdmin=!!a.isAdmin,n.downloads=a.downloads,a.downloadClients){n.downloadClients=a.downloadClients;const r=new CustomEvent("downloadClientsUpdated");document.dispatchEvent(r)}if(a.ombiRequests){n.ombiRequests=a.ombiRequests;const r=new CustomEvent("ombiRequestsUpdated");document.dispatchEvent(r)}a.ombiBaseUrl&&(n.ombiBaseUrl=a.ombiBaseUrl),document.getElementById("currentUser").textContent=n.currentUser||"-",oe(),yt(),s&&(s=!1,vt())}catch(a){console.error("[SSE] Failed to parse message:",a)}},t.addEventListener("history-update",o=>{try{const a=JSON.parse(o.data);console.log("[SSE] History update received:",a.type);const r=new CustomEvent("historyReload");document.dispatchEvent(r)}catch(a){console.error("[SSE] Failed to parse history-update message:",a)}}),t.onerror=()=>{console.warn("[SSE] Connection lost, browser will retry...")},console.log("[SSE] Stream connected")}function de(){n.sseSource&&(n.sseSource.close(),n.sseSource=null,console.log("[SSE] Stream closed"))}function Ze(e){n.showAll=e,$();const t=new CustomEvent("historyReload");document.dispatchEvent(t)}function et(){document.getElementById("webhooks-section")&&(document.getElementById("webhooks-header").addEventListener("click",tt),document.getElementById("enable-sonarr-webhook").addEventListener("click",nt),document.getElementById("enable-radarr-webhook").addEventListener("click",ot),document.getElementById("enable-ombi-webhook").addEventListener("click",it),document.getElementById("test-sonarr-webhook").addEventListener("click",at),document.getElementById("test-radarr-webhook").addEventListener("click",rt),document.getElementById("test-ombi-webhook").addEventListener("click",lt))}function tt(){n.webhookSectionExpanded=!n.webhookSectionExpanded;const e=document.getElementById("webhooks-content"),t=document.getElementById("webhooks-toggle");n.webhookSectionExpanded?e.classList.remove("hidden"):e.classList.add("hidden"),t.classList.toggle("expanded",n.webhookSectionExpanded),n.webhookSectionExpanded&&ue()}async function ue(){const e=document.getElementById("webhook-loading");e.classList.remove("hidden");try{(await L()).success&&st()}catch(t){console.error("Failed to fetch webhook status:",t)}finally{e.classList.add("hidden")}}function st(){const e=document.getElementById("sonarr-status"),t=document.getElementById("enable-sonarr-webhook"),s=document.getElementById("test-sonarr-webhook"),o=document.getElementById("sonarr-triggers"),a=document.getElementById("sonarr-stats");e.textContent=n.sonarrWebhook.enabled?"● Enabled":"○ Disabled",e.className="status-indicator "+(n.sonarrWebhook.enabled?"enabled":"disabled"),n.sonarrWebhook.enabled?(t.classList.add("hidden"),s.classList.remove("hidden"),o.classList.remove("hidden")):(t.classList.remove("hidden"),s.classList.add("hidden"),o.classList.add("hidden")),n.sonarrWebhook.enabled&&(document.getElementById("sonarr-onGrab").textContent=n.sonarrWebhook.triggers.onGrab?"✓":"✗",document.getElementById("sonarr-onGrab").className="trigger-value "+(n.sonarrWebhook.triggers.onGrab?"active":"inactive"),document.getElementById("sonarr-onDownload").textContent=n.sonarrWebhook.triggers.onDownload?"✓":"✗",document.getElementById("sonarr-onDownload").className="trigger-value "+(n.sonarrWebhook.triggers.onDownload?"active":"inactive"),document.getElementById("sonarr-onImport").textContent=n.sonarrWebhook.triggers.onImport?"✓":"✗",document.getElementById("sonarr-onImport").className="trigger-value "+(n.sonarrWebhook.triggers.onImport?"active":"inactive"),document.getElementById("sonarr-onUpgrade").textContent=n.sonarrWebhook.triggers.onUpgrade?"✓":"✗",document.getElementById("sonarr-onUpgrade").className="trigger-value "+(n.sonarrWebhook.triggers.onUpgrade?"active":"inactive")),n.sonarrWebhook.stats?(a.classList.remove("hidden"),document.getElementById("sonarr-events").textContent=n.sonarrWebhook.stats.eventsReceived??0,document.getElementById("sonarr-polls").textContent=n.sonarrWebhook.stats.pollsSkipped??0,document.getElementById("sonarr-last").textContent=D(n.sonarrWebhook.stats.lastWebhookTimestamp)):a.classList.add("hidden");const r=document.getElementById("radarr-status"),c=document.getElementById("enable-radarr-webhook"),m=document.getElementById("test-radarr-webhook"),d=document.getElementById("radarr-triggers"),l=document.getElementById("radarr-stats");r.textContent=n.radarrWebhook.enabled?"● Enabled":"○ Disabled",r.className="status-indicator "+(n.radarrWebhook.enabled?"enabled":"disabled"),n.radarrWebhook.enabled?(c.classList.add("hidden"),m.classList.remove("hidden"),d.classList.remove("hidden")):(c.classList.remove("hidden"),m.classList.add("hidden"),d.classList.add("hidden")),n.radarrWebhook.enabled&&(document.getElementById("radarr-onGrab").textContent=n.radarrWebhook.triggers.onGrab?"✓":"✗",document.getElementById("radarr-onGrab").className="trigger-value "+(n.radarrWebhook.triggers.onGrab?"active":"inactive"),document.getElementById("radarr-onDownload").textContent=n.radarrWebhook.triggers.onDownload?"✓":"✗",document.getElementById("radarr-onDownload").className="trigger-value "+(n.radarrWebhook.triggers.onDownload?"active":"inactive"),document.getElementById("radarr-onImport").textContent=n.radarrWebhook.triggers.onImport?"✓":"✗",document.getElementById("radarr-onImport").className="trigger-value "+(n.radarrWebhook.triggers.onImport?"active":"inactive"),document.getElementById("radarr-onUpgrade").textContent=n.radarrWebhook.triggers.onUpgrade?"✓":"✗",document.getElementById("radarr-onUpgrade").className="trigger-value "+(n.radarrWebhook.triggers.onUpgrade?"active":"inactive")),n.radarrWebhook.stats?(l.classList.remove("hidden"),document.getElementById("radarr-events").textContent=n.radarrWebhook.stats.eventsReceived??0,document.getElementById("radarr-polls").textContent=n.radarrWebhook.stats.pollsSkipped??0,document.getElementById("radarr-last").textContent=D(n.radarrWebhook.stats.lastWebhookTimestamp)):l.classList.add("hidden");const f=document.getElementById("ombi-status"),i=document.getElementById("enable-ombi-webhook"),u=document.getElementById("test-ombi-webhook"),p=document.getElementById("ombi-triggers"),h=document.getElementById("ombi-stats");f.textContent=n.ombiWebhook.enabled?"● Enabled":"○ Disabled",f.className="status-indicator "+(n.ombiWebhook.enabled?"enabled":"disabled"),n.ombiWebhook.enabled?(i.classList.add("hidden"),u.classList.remove("hidden"),p.classList.remove("hidden")):(i.classList.remove("hidden"),u.classList.add("hidden"),p.classList.add("hidden")),n.ombiWebhook.enabled&&(document.getElementById("ombi-requestAvailable").textContent=n.ombiWebhook.triggers.requestAvailable?"✓":"✗",document.getElementById("ombi-requestAvailable").className="trigger-value "+(n.ombiWebhook.triggers.requestAvailable?"active":"inactive"),document.getElementById("ombi-requestApproved").textContent=n.ombiWebhook.triggers.requestApproved?"✓":"✗",document.getElementById("ombi-requestApproved").className="trigger-value "+(n.ombiWebhook.triggers.requestApproved?"active":"inactive"),document.getElementById("ombi-requestDeclined").textContent=n.ombiWebhook.triggers.requestDeclined?"✓":"✗",document.getElementById("ombi-requestDeclined").className="trigger-value "+(n.ombiWebhook.triggers.requestDeclined?"active":"inactive"),document.getElementById("ombi-requestPending").textContent=n.ombiWebhook.triggers.requestPending?"✓":"✗",document.getElementById("ombi-requestPending").className="trigger-value "+(n.ombiWebhook.triggers.requestPending?"active":"inactive"),document.getElementById("ombi-requestProcessing").textContent=n.ombiWebhook.triggers.requestProcessing?"✓":"✗",document.getElementById("ombi-requestProcessing").className="trigger-value "+(n.ombiWebhook.triggers.requestProcessing?"active":"inactive")),n.ombiWebhook.stats?(h.classList.remove("hidden"),document.getElementById("ombi-events").textContent=n.ombiWebhook.stats.eventsReceived??0,document.getElementById("ombi-polls").textContent=n.ombiWebhook.stats.pollsSkipped??0,document.getElementById("ombi-last").textContent=D(n.ombiWebhook.stats.lastWebhookTimestamp)):h.classList.add("hidden")}async function nt(){v(!0);try{const e=await Te();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{v(!1)}}async function ot(){v(!0);try{const e=await xe();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{v(!1)}}async function at(){v(!0);try{const e=await Ne();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{v(!1)}}async function rt(){v(!0);try{const e=await Re();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{v(!1)}}async function it(){v(!0);try{const e=await De();e.success||(console.error("Failed to enable Ombi webhook:",e.error),alert("Failed to enable Ombi webhook. Check console for details."))}catch(e){console.error("Failed to enable Ombi webhook:",e),alert("Failed to enable Ombi webhook. Check console for details.")}finally{v(!1)}}async function lt(){v(!0);try{const e=await Ae();e.success?alert("Ombi webhook test sent successfully!"):(console.error("Failed to test Ombi webhook:",e.error),alert("Failed to test Ombi webhook. Check console for details."))}catch(e){console.error("Failed to test Ombi webhook:",e),alert("Failed to test Ombi webhook. Check console for details.")}finally{v(!1)}}function v(e){n.webhookLoading=e,document.getElementById("enable-sonarr-webhook").disabled=e,document.getElementById("enable-radarr-webhook").disabled=e,document.getElementById("enable-ombi-webhook").disabled=e,document.getElementById("test-sonarr-webhook").disabled=e,document.getElementById("test-radarr-webhook").disabled=e,document.getElementById("test-ombi-webhook").disabled=e;const t=document.getElementById("webhook-loading");e?t.classList.remove("hidden"):t.classList.add("hidden")}async function ct(){const e=document.getElementById("status-panel"),t=document.getElementById("webhooks-section");if(!e.classList.contains("hidden")){e.classList.add("hidden"),t&&t.classList.add("hidden"),n.statusRefreshHandle&&(clearInterval(n.statusRefreshHandle),n.statusRefreshHandle=null);return}e.classList.remove("hidden"),t&&n.isAdmin?(t.classList.remove("hidden"),n.webhookSectionExpanded=!1,document.getElementById("webhooks-content").classList.add("hidden"),document.getElementById("webhooks-toggle").classList.remove("expanded"),await ue()):t&&t.classList.add("hidden"),X(),n.statusRefreshHandle&&clearInterval(n.statusRefreshHandle),n.statusRefreshHandle=setInterval(X,ke)}function dt(){document.getElementById("status-panel").classList.add("hidden");const e=document.getElementById("webhooks-section");e&&e.classList.add("hidden"),n.statusRefreshHandle&&(clearInterval(n.statusRefreshHandle),n.statusRefreshHandle=null)}async function X(){var s;const e=document.getElementById("status-panel"),t=document.getElementById("status-content");if(console.log("[Status] panel found:",!!e,"contentDiv found:",!!t,"panel display:",(s=e==null?void 0:e.style)==null?void 0:s.display),!(!e||e.classList.contains("hidden"))){console.log("[Status] Refreshing status panel...");try{const o=await Fe();o.success?(console.log("[Status] Got status data, rendering..."),ut(o.data,e)):(console.error("[Status] API returned error:",o.error),t&&(!t.innerHTML||t.innerHTML.includes("status-loading"))&&(t.innerHTML='<p class="status-error">Failed to load status: '+o.error+"</p>"))}catch(o){console.error("[Status] Error fetching status:",o),t&&(!t.innerHTML||t.innerHTML.includes("status-loading"))&&(t.innerHTML='<p class="status-error">Failed to load status: '+o.message+"</p>")}}}function ut(e,t){var E,T,k,U,P,O,j,_,G;console.log("[Status] renderStatusPanel called with data:",e?"yes":"no","keys:",e?Object.keys(e):"none");const s=e.server,o=Math.floor(s.uptimeSeconds/3600),a=Math.floor(s.uptimeSeconds%3600/60),r=s.uptimeSeconds%60,c=`${o}h ${a}m ${r}s`,m=(e.cache.totalSizeBytes/1024).toFixed(1);let d=`
|
|
<div class="status-header">
|
|
<h3>Server Status</h3>
|
|
<button class="status-close" id="status-close-btn">×</button>
|
|
</div>
|
|
<div class="status-grid">
|
|
<div class="status-card">
|
|
<div class="status-card-title">Server</div>
|
|
<div class="status-row"><span>Uptime</span><span>${c}</span></div>
|
|
<div class="status-row"><span>Node</span><span>${N(s.nodeVersion)}</span></div>
|
|
<div class="status-row"><span>Memory (RSS)</span><span>${s.memoryUsageMB} MB</span></div>
|
|
<div class="status-row"><span>Heap</span><span>${s.heapUsedMB} / ${s.heapTotalMB} MB</span></div>
|
|
</div>
|
|
<div class="status-card">
|
|
<div class="status-card-title">Data Refresh</div>`;const l=e.polling.intervalMs,i=(e.clients||[]).filter(g=>g.type==="sse");e.polling.enabled?d+=`<div class="status-row"><span>Background poll</span><span>${l/1e3}s</span></div>`:d+='<div class="status-row"><span>Background poll</span><span>Disabled</span></div>';const u=i.length>0?'<span class="status-fg-badge">SSE push</span>':e.polling.enabled?"Background":"On-demand (idle)";d+=`<div class="status-row"><span>Delivery mode</span><span>${u}</span></div>`,d+=`<div class="status-row"><span>SSE clients</span><span>${i.length}</span></div>`;for(const g of i){const w=Math.round((Date.now()-g.connectedAt)/1e3);d+=`<div class="status-row status-row-sub"><span>${N(g.user)}</span><span>connected ${w}s ago</span></div>`}if(d+="</div>",n.isAdmin&&e.webhooks){const g=e.webhooks,w=(E=g.sonarr)!=null&&E.enabled?"●":"○",S=(T=g.radarr)!=null&&T.enabled?"●":"○",C=((k=g.sonarr)==null?void 0:k.eventsReceived)||0,ge=((U=g.radarr)==null?void 0:U.eventsReceived)||0,be=((P=g.sonarr)==null?void 0:P.pollsSkipped)||0,ye=((O=g.radarr)==null?void 0:O.pollsSkipped)||0;d+=`
|
|
<div class="status-card">
|
|
<div class="status-card-title">Webhooks</div>
|
|
<div class="status-row"><span>Sonarr</span><span>${w} ${(j=g.sonarr)!=null&&j.enabled?"Enabled":"Disabled"}</span></div>
|
|
<div class="status-row"><span>Radarr</span><span>${S} ${(_=g.radarr)!=null&&_.enabled?"Enabled":"Disabled"}</span></div>
|
|
<div class="status-row status-row-sub"><span>Events</span><span>S:${C} R:${ge}</span></div>
|
|
<div class="status-row status-row-sub"><span>Polls skipped</span><span>S:${be} R:${ye}</span></div>
|
|
</div>`}const p=e.polling.lastPoll;if(p){const g=Math.round((Date.now()-new Date(p.timestamp).getTime())/1e3);d+=`
|
|
<div class="status-card status-card-wide">
|
|
<div class="status-card-title">Last Poll (${p.totalMs}ms total, ${g}s ago)</div>
|
|
<div class="status-timings">`;const w=p.tasks.reduce((S,C)=>Math.max(S,C.ms),1);for(const S of p.tasks){const C=Math.max(2,S.ms/w*100);d+=`
|
|
<div class="timing-row">
|
|
<span class="timing-label">${N(S.label)}</span>
|
|
<div class="timing-bar-bg"><div class="timing-bar" data-w="${C.toFixed(1)}"></div></div>
|
|
<span class="timing-value">${S.ms}ms</span>
|
|
</div>`}d+="</div></div>"}d+=`
|
|
<div class="status-card status-card-wide">
|
|
<div class="status-card-title">Cache (${e.cache.entryCount} entries, ${m} KB)</div>
|
|
<table class="status-table">
|
|
<thead><tr><th>Key</th><th>Items</th><th>Size</th><th>TTL</th></tr></thead>
|
|
<tbody>`;for(const g of e.cache.entries){const w=g.sizeBytes>1024?(g.sizeBytes/1024).toFixed(1)+" KB":g.sizeBytes+" B",S=g.expired?'<span class="status-expired">expired</span>':(g.ttlRemainingMs/1e3).toFixed(0)+"s",C=g.itemCount!==null?g.itemCount:"—";d+=`<tr><td><code>${N(g.key)}</code></td><td>${C}</td><td>${w}</td><td>${S}</td></tr>`}d+="</tbody></table></div></div>";const h=document.getElementById("status-content"),y=document.getElementById("status-panel");console.log("[Status] contentDiv found:",!!h,"panel children:",(G=y==null?void 0:y.children)==null?void 0:G.length,"HTML length:",d.length),y&&console.log("[Status] panel innerHTML preview:",y.innerHTML.substring(0,200)),h?(h.innerHTML=d,console.log("[Status] HTML rendered, contentDiv innerHTML length:",h.innerHTML.length)):console.error("[Status] contentDiv not found!");const b=document.getElementById("status-close-btn");b&&b.addEventListener("click",dt),t.querySelectorAll(".timing-bar[data-w]").forEach(g=>{g.style.width=g.dataset.w+"%"})}function N(e){const t=document.createElement("div");return t.textContent=e,t.innerHTML}function mt(){return new Promise(e=>{const t=document.getElementById("login-container");t.classList.add("fade-out"),t.addEventListener("transitionend",()=>{t.classList.add("hidden"),t.classList.remove("fade-out"),e()},{once:!0})})}function ht(){const e=document.getElementById("splash-screen");e.classList.remove("hidden"),e.style.opacity="1",e.classList.remove("fade-out")}function R(e){return new Promise(t=>{const s=Date.now()-(e||0),o=Math.max(0,ve-s);setTimeout(()=>{const a=document.getElementById("splash-screen");a.classList.add("fade-out");const c=setTimeout(()=>{a.classList.add("hidden"),t()},400+100);a.addEventListener("transitionend",()=>{clearTimeout(c),a.classList.add("hidden"),t()},{once:!0})},o)})}async function pt(){const e=Date.now();try{(await Se()).authenticated?(me(),he(),$(),await R(e)):(await R(e),F())}catch(t){console.error("Authentication check failed:",t),await R(e),F()}}async function ft(e){e.preventDefault();const t=document.getElementById("username").value,s=document.getElementById("password").value,o=document.getElementById("remember-me").checked;try{const a=await we(t,s,o);if(a.success){await mt(),ht(),await new Promise(c=>requestAnimationFrame(()=>requestAnimationFrame(c))),me(),he();const r=Date.now();$(),await R(r)}else V(a.error||"Login failed")}catch(a){V("Login failed. Please try again."),console.error(a)}}async function gt(){try{de(),le(),n.statusRefreshHandle&&(clearInterval(n.statusRefreshHandle),n.statusRefreshHandle=null),await Ce(),n.currentUser=null,Ve(),F()}catch(e){console.error("Logout failed:",e)}}function F(){document.getElementById("login-container").classList.remove("hidden"),document.getElementById("dashboard-container").classList.add("hidden"),bt()}function me(){document.getElementById("login-container").classList.add("hidden"),document.getElementById("dashboard-container").classList.remove("hidden"),document.getElementById("currentUser").textContent=n.currentUser.name||"-",document.getElementById("status-panel").classList.add("hidden");const t=document.getElementById("webhooks-section");t&&t.classList.add("hidden");const s=document.getElementById("admin-controls");n.isAdmin?s.classList.remove("hidden"):s.classList.add("hidden");const o=document.getElementById("history-days");o&&(o.value=n.historyDays),Xe()}function V(e){const t=document.getElementById("login-error");t.textContent=e,t.classList.remove("hidden")}function bt(){document.getElementById("login-error").classList.add("hidden")}function yt(){document.getElementById("error-message").classList.add("hidden")}function he(){document.getElementById("loading").classList.remove("hidden")}function vt(){document.getElementById("loading").classList.add("hidden")}function Et(){const e=document.getElementById("download-client-filter-btn"),t=document.getElementById("download-client-filter-dropdown"),s=document.getElementById("download-client-filter-close");!e||!t||(e.addEventListener("click",o=>{o.stopPropagation(),t.classList.toggle("open")}),s.addEventListener("click",()=>{t.classList.remove("open")}),document.addEventListener("click",o=>{!t.contains(o.target)&&o.target!==e&&t.classList.remove("open")}),document.addEventListener("downloadClientsUpdated",Y),Y())}function Y(){const e=document.getElementById("download-client-filter-list");e&&(e.innerHTML="",n.downloadClients.forEach((t,s)=>{const o=document.createElement("div");o.className="filter-item",o.dataset.index=s;const a=document.createElement("input");a.type="checkbox",a.id=`client-${s}`,a.checked=n.selectedDownloadClients.includes(s),a.addEventListener("change",()=>kt(s));const r=document.createElement("label");r.htmlFor=`client-${s}`,r.textContent=t.name||`${t.type} (${t.id})`,o.appendChild(a),o.appendChild(r),e.appendChild(o)}),pe())}function kt(e){const t=n.selectedDownloadClients.indexOf(e);t>-1?n.selectedDownloadClients.splice(t,1):n.selectedDownloadClients.push(e),_e(n.selectedDownloadClients),pe(),oe()}function pe(){const e=document.getElementById("download-client-filter-count");e&&(n.selectedDownloadClients.length===0?e.textContent="All":e.textContent=n.selectedDownloadClients.length)}function fe(e){return e?e.available?"available":e.denied?"denied":e.approved?"approved":e.requested?"pending":"unknown":"unknown"}function St(e,t){if(!t||t.length===0)return e;const s=t.map(o=>o.toLowerCase());return s.includes("all")?e:e.filter(o=>s.includes(o.mediaType))}function wt(e,t){if(!t||t.length===0)return e;const s=t.map(o=>o.toLowerCase());return e.filter(o=>s.includes(fe(o)))}function Ct(e,t){if(!t||t.trim()==="")return e;const s=t.trim().toLowerCase();return e.filter(o=>(o.title||"").toLowerCase().includes(s))}function It(e,t){const s=[...e];switch(t){case"requestedDate_asc":return s.sort((o,a)=>{const r=o.requestedDate?new Date(o.requestedDate).getTime():0,c=a.requestedDate?new Date(a.requestedDate).getTime():0;return r-c});case"title_asc":return s.sort((o,a)=>(o.title||"").localeCompare(a.title||""));case"title_desc":return s.sort((o,a)=>(a.title||"").localeCompare(o.title||""));case"requestedDate_desc":default:return s.sort((o,a)=>{const r=o.requestedDate?new Date(o.requestedDate).getTime():0;return(a.requestedDate?new Date(a.requestedDate).getTime():0)-r})}}function Bt(e,{types:t,statuses:s,sort:o,search:a}={}){let r=[...e];return r=St(r,t),r=wt(r,s),r=Ct(r,a),r=It(r,o),r}function Lt(e){return e?e.requestedUser&&typeof e.requestedUser=="object"?e.requestedUser.alias||e.requestedUser.userAlias||e.requestedUser.userName||e.requestedUser.normalizedUserName||"":e.requestedUser||e.requestedByAlias||"":""}function q(){const e=document.getElementById("requests-list"),t=document.getElementById("no-requests");if(!e)return;const s=n.ombiRequests||{movie:[],tv:[]},o=[...s.movie.map(r=>({...r,mediaType:"movie"})),...s.tv.map(r=>({...r,mediaType:"tv"}))],a=Bt(o,{types:n.selectedRequestTypes,statuses:n.selectedRequestStatuses,sort:n.requestSortMode,search:n.requestSearchQuery});if(e.innerHTML="",a.length===0){if(t){t.style.display="block";const r=t.querySelector("p");if(r){const c=o.length>0;r.textContent=c?"No requests match your filters.":"No requests found."}}return}t&&(t.style.display="none"),a.forEach(r=>{const c=qt(r);e.appendChild(c)})}function qt(e){const t=document.createElement("div");t.className="request-card";const s=document.createElement("span");s.className=`request-type-icon ${e.mediaType}`,s.textContent=e.mediaType==="movie"?"🎬":"📺";const o=document.createElement("div");o.className="request-content";const a=document.createElement("div");a.className="request-title",a.textContent=e.title||"Unknown Title";const r=document.createElement("div");r.className="request-meta";const c=Tt(e);if(r.appendChild(c),e.year){const l=document.createElement("span");l.className="request-year",l.textContent=e.year,r.appendChild(l)}const m=Lt(e);if(m){const l=document.createElement("span");l.className="request-user",l.textContent=`Requested by: ${m}`,r.appendChild(l)}if(e.quality){const l=document.createElement("span");l.className="request-quality",l.textContent=e.quality,r.appendChild(l)}o.appendChild(a),o.appendChild(r);const d=document.createElement("div");if(d.className="request-actions",n.ombiBaseUrl&&e.theMovieDbId){const l=document.createElement("a");l.className="request-link ombi-link",l.href=`${n.ombiBaseUrl}/details/${e.mediaType}/${e.theMovieDbId}`,l.target="_blank",l.title="View in Ombi";const f=document.createElement("img");f.src="/images/ombi.svg",f.alt="Ombi",f.className="request-icon",l.appendChild(f),d.appendChild(l)}return t.appendChild(s),t.appendChild(o),t.appendChild(d),t}function Tt(e){const t=document.createElement("span");t.className="request-status-badge";const s=fe(e),o={available:"Available",denied:`Denied: ${e.deniedReason||"No reason"}`,approved:"Approved",pending:"Pending",unknown:"Unknown"};return t.classList.add(s),t.textContent=o[s]||"Unknown",t}function xt(){const e=document.getElementById("request-type-filter-btn"),t=document.getElementById("request-type-filter-dropdown"),s=document.getElementById("request-type-select-all"),o=document.getElementById("request-type-deselect-all");if(!e||!t)return;e.addEventListener("click",r=>{r.stopPropagation(),t.classList.toggle("open")}),s==null||s.addEventListener("click",()=>Z(!0)),o==null||o.addEventListener("click",()=>Z(!1)),t.querySelectorAll(".request-filter-checkbox").forEach(r=>{r.addEventListener("change",()=>{const c=r.closest(".request-filter-option").dataset.value;Nt(c,r.checked)})}),W()}function Z(e){const s=document.getElementById("request-type-filter-dropdown").querySelectorAll(".request-filter-checkbox"),o=[];s.forEach(a=>{a.checked=e,e&&o.push(a.closest(".request-filter-option").dataset.value)}),n.selectedRequestTypes=e?o:[],re(n.selectedRequestTypes),W(),q()}function Nt(e,t){const s=n.selectedRequestTypes.indexOf(e);t&&s===-1?n.selectedRequestTypes.push(e):!t&&s>-1&&n.selectedRequestTypes.splice(s,1),re(n.selectedRequestTypes),W(),q()}function W(){const e=document.getElementById("request-type-selected-text");if(!e)return;const s=document.getElementById("request-type-filter-dropdown").querySelectorAll(".request-filter-checkbox");s.forEach(o=>{const a=o.closest(".request-filter-option").dataset.value;o.checked=n.selectedRequestTypes.includes(a)}),n.selectedRequestTypes.length===0?e.textContent="None":n.selectedRequestTypes.length===s.length?e.textContent="All":e.textContent=n.selectedRequestTypes.length}function Rt(){const e=document.getElementById("request-status-filter-btn"),t=document.getElementById("request-status-filter-dropdown"),s=document.getElementById("request-status-select-all"),o=document.getElementById("request-status-deselect-all");if(!e||!t)return;e.addEventListener("click",r=>{r.stopPropagation(),t.classList.toggle("open")}),s==null||s.addEventListener("click",()=>ee(!0)),o==null||o.addEventListener("click",()=>ee(!1)),t.querySelectorAll(".request-filter-checkbox").forEach(r=>{r.addEventListener("change",()=>{const c=r.closest(".request-filter-option").dataset.value;Dt(c,r.checked)})}),H()}function ee(e){const s=document.getElementById("request-status-filter-dropdown").querySelectorAll(".request-filter-checkbox"),o=[];s.forEach(a=>{a.checked=e,e&&o.push(a.closest(".request-filter-option").dataset.value)}),n.selectedRequestStatuses=e?o:[],ie(n.selectedRequestStatuses),H(),q()}function Dt(e,t){const s=n.selectedRequestStatuses.indexOf(e);t&&s===-1?n.selectedRequestStatuses.push(e):!t&&s>-1&&n.selectedRequestStatuses.splice(s,1),ie(n.selectedRequestStatuses),H(),q()}function H(){const e=document.getElementById("request-status-selected-text");if(!e)return;const s=document.getElementById("request-status-filter-dropdown").querySelectorAll(".request-filter-checkbox");s.forEach(o=>{const a=o.closest(".request-filter-option").dataset.value;o.checked=n.selectedRequestStatuses.includes(a)}),n.selectedRequestStatuses.length===0||n.selectedRequestStatuses.length===s.length?e.textContent="All":e.textContent=n.selectedRequestStatuses.length}function At(){const e=document.getElementById("request-sort-select");e&&(e.value=n.requestSortMode,e.addEventListener("change",t=>{n.requestSortMode=t.target.value,Je(n.requestSortMode),q()}))}function Ft(){const e=document.getElementById("request-search-input");if(!e)return;e.value=n.requestSearchQuery;let t;e.addEventListener("input",s=>{clearTimeout(t),t=setTimeout(()=>{n.requestSearchQuery=s.target.value,Qe(n.requestSearchQuery),q()},200)})}function Mt(){document.addEventListener("click",e=>{const t=document.getElementById("request-type-filter-dropdown"),s=document.getElementById("request-type-filter-btn"),o=document.getElementById("request-status-filter-dropdown"),a=document.getElementById("request-status-filter-btn");t&&!t.contains(e.target)&&e.target!==s&&!(s!=null&&s.contains(e.target))&&t.classList.remove("open"),o&&!o.contains(e.target)&&e.target!==a&&!(a!=null&&a.contains(e.target))&&o.classList.remove("open")})}function $t(){xt(),Rt(),At(),Ft(),Mt(),document.addEventListener("ombiRequestsUpdated",()=>{q()})}(function(){const t=ae();t&&document.documentElement.setAttribute("data-theme",t)})();function Wt(){const e=document.getElementById("theme-toggle");e&&e.addEventListener("click",()=>{const s=ae()==="dark"?"light":"dark";Ht(s)})}function Ht(e){document.documentElement.setAttribute("data-theme",e),Ge(e)}function Ut(){const e=document.querySelector('[data-tab="downloads"]'),t=document.querySelector('[data-tab="requests"]'),s=document.querySelector('[data-tab="history"]');if(!e||!s)return;const o=ze();B(o==="requests"?"requests":o==="history"?"history":"downloads"),e.addEventListener("click",()=>B("downloads")),t&&t.addEventListener("click",()=>B("requests")),s.addEventListener("click",()=>B("history"))}function B(e){const t=document.querySelector('[data-tab="downloads"]'),s=document.querySelector('[data-tab="requests"]'),o=document.querySelector('[data-tab="history"]'),a=document.getElementById("tab-downloads"),r=document.getElementById("tab-requests"),c=document.getElementById("tab-history");t&&t.classList.remove("active"),s&&s.classList.remove("active"),o&&o.classList.remove("active"),a&&a.classList.add("hidden"),r&&r.classList.add("hidden"),c&&c.classList.add("hidden"),e==="downloads"?(t&&t.classList.add("active"),a&&a.classList.remove("hidden"),A("downloads")):e==="requests"&&(s&&s.classList.add("active"),r&&r.classList.remove("hidden"),A("requests"),setupRequestsTab(),o&&o.classList.add("active"),c&&c.classList.remove("hidden"),A("history"),x())}function Pt(){B("downloads")}document.addEventListener("DOMContentLoaded",()=>{const e=document.getElementById("login-form");e&&e.addEventListener("submit",ft);const t=document.getElementById("logout-btn");t&&t.addEventListener("click",gt);const s=document.getElementById("show-all-toggle");s&&s.addEventListener("change",r=>Ze(r.target.checked));const o=document.getElementById("status-btn");o&&o.addEventListener("click",ct);const a=document.getElementById("home-btn");a&&a.addEventListener("click",Pt),Wt(),Ut(),Et(),$t(),Ke(),et(),Le().then(r=>{const c=document.getElementById("app-version");c&&r&&(c.textContent="v"+r)}),pt()});
|