qBitrr2 5.8.6__py3-none-any.whl → 5.8.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qBitrr/arss.py +61 -0
- qBitrr/bundled_data.py +2 -2
- qBitrr/config_version.py +1 -1
- qBitrr/database.py +29 -1
- qBitrr/gen_config.py +160 -0
- qBitrr/main.py +229 -0
- qBitrr/qbit_category_manager.py +293 -0
- qBitrr/static/assets/ArrView.js +1 -1
- qBitrr/static/assets/ArrView.js.map +1 -1
- qBitrr/static/assets/ConfigView.js +5 -5
- qBitrr/static/assets/ConfigView.js.map +1 -1
- qBitrr/static/assets/LogsView.js +9 -9
- qBitrr/static/assets/LogsView.js.map +1 -1
- qBitrr/static/assets/ProcessesView.js +1 -1
- qBitrr/static/assets/ProcessesView.js.map +1 -1
- qBitrr/static/assets/QbitCategoriesView.js +2 -0
- qBitrr/static/assets/QbitCategoriesView.js.map +1 -0
- qBitrr/static/assets/StableTable.js +2 -0
- qBitrr/static/assets/StableTable.js.map +1 -0
- qBitrr/static/assets/app.css +1 -1
- qBitrr/static/assets/app.js +23 -10
- qBitrr/static/assets/app.js.map +1 -1
- qBitrr/static/assets/table.js +1 -1
- qBitrr/static/assets/vendor.js +1 -1
- qBitrr/static/assets/vendor.js.map +1 -1
- qBitrr/webui.py +208 -2
- {qbitrr2-5.8.6.dist-info → qbitrr2-5.8.8.dist-info}/METADATA +3 -3
- {qbitrr2-5.8.6.dist-info → qbitrr2-5.8.8.dist-info}/RECORD +32 -27
- {qbitrr2-5.8.6.dist-info → qbitrr2-5.8.8.dist-info}/WHEEL +0 -0
- {qbitrr2-5.8.6.dist-info → qbitrr2-5.8.8.dist-info}/entry_points.txt +0 -0
- {qbitrr2-5.8.6.dist-info → qbitrr2-5.8.8.dist-info}/licenses/LICENSE +0 -0
- {qbitrr2-5.8.6.dist-info → qbitrr2-5.8.8.dist-info}/top_level.txt +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as e,I as
|
|
1
|
+
import{j as e,I as q,C as Y,u as Z,g as ee,a as se,b as re,r as K,c as te,d as ae,R as B}from"./app.js";import{r as l}from"./table.js";import{u as ne}from"./useInterval.js";import"./vendor.js";function ce({title:t,message:n,confirmLabel:h="Confirm",cancelLabel:y="Cancel",onConfirm:C,onCancel:j,danger:$=!1}){return e.jsx("div",{className:"modal-backdrop",onClick:j,children:e.jsxs("div",{className:"modal",style:{maxWidth:"500px"},onClick:N=>N.stopPropagation(),children:[e.jsxs("div",{className:"modal-header",children:[e.jsx("h2",{children:t}),e.jsx("button",{className:"btn ghost",onClick:j,children:e.jsx(q,{src:Y})})]}),e.jsx("div",{className:"modal-body",children:e.jsx("p",{style:{margin:0,lineHeight:1.6},children:n})}),e.jsxs("div",{className:"modal-footer",children:[e.jsx("button",{className:"btn ghost",onClick:j,children:y}),e.jsx("button",{className:`btn ${$?"danger":"primary"}`,onClick:C,children:h})]})]})})}const oe="/static/assets/build.svg",ie=/\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\b/i,le=/\bS\d{1,3}E\d{1,3}\b/i,de=/\bSeason\s+\d+\b/i;function ue(t){const n=t.trim();if(!n)return"";if(/^\d+\s+queued item/i.test(n))return n;const h=n.replace(/\s+/g," "),y=h.match(/^(?<title>.+?)\s+(?<year>(?:19|20)\d{2})(?:\s+(?<rest>.*))?$/);if(y){const C=y.groups?.rest??"",j=le.test(C)||de.test(C);if(C&&!j&&ie.test(C)){const N=(y.groups?.title??"").replace(/[-_.]/g," ").replace(/\s{2,}/g," ").trim(),E=y.groups?.year??"";if(N)return E?`${N} (${E})`:N}}return h}function me(t,n){return t.category===n.category&&t.name===n.name&&t.kind===n.kind&&t.pid===n.pid&&t.alive===n.alive&&(t.rebuilding??!1)===(n.rebuilding??!1)&&(t.searchSummary??"")===(n.searchSummary??"")&&(t.searchTimestamp??"")===(n.searchTimestamp??"")&&(t.queueCount??null)===(n.queueCount??null)&&(t.categoryCount??null)===(n.categoryCount??null)&&(t.metricType??"")===(n.metricType??"")}function pe(t,n){if(t===n)return!0;if(t.length!==n.length)return!1;for(let h=0;h<t.length;h+=1)if(!me(t[h],n[h]))return!1;return!0}function he(t){return t?1e3:null}function je({active:t}){const[n,h]=l.useState([]),[y,C]=l.useState(!1),[j,$]=l.useState(!1),[N,E]=l.useState(!1),[T,G]=l.useState(null),[z,X]=l.useState([]),[k,S]=l.useState(null),{push:d}=Z(),P=l.useRef(!1),u=l.useCallback(async(a=!0)=>{if(!P.current){P.current=!0,a&&C(!0);try{const[c,x,A]=await Promise.all([ee(),se(),re().catch(()=>null)]),b=(c.processes??[]).map(g=>{if(typeof g.searchSummary=="string"){const v=ue(g.searchSummary);return{...g,searchSummary:v}}return g});h(g=>pe(g,b)?g:b),G(x),A?.categories&&X(A.categories)}catch(c){d(c instanceof Error?c.message:"Failed to load processes list","error")}finally{P.current=!1,a&&C(!1)}}},[d]);l.useEffect(()=>{u()},[u]),l.useEffect(()=>{t&&u()},[t,u]);const W=l.useMemo(()=>he(t),[t]);ne(()=>{u(!1)},W);const Q=l.useCallback(async(a,c)=>{try{await K(a,c),d(`Restarted ${a}:${c}`,"success"),u()}catch(x){d(x instanceof Error?x.message:`Failed to restart ${a}:${c}`,"error")}},[u,d]),H=l.useCallback(async()=>{S({title:"Restart All Processes",message:"Are you sure you want to restart all processes? This will temporarily interrupt all operations.",onConfirm:async()=>{S(null),$(!0);try{await te(),d("Restarted all processes","success"),u()}catch(a){d(a instanceof Error?a.message:"Failed to restart all","error")}finally{$(!1)}}})},[u,d]),U=l.useCallback(async()=>{S({title:"Rebuild Arrs",message:"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.",onConfirm:async()=>{S(null),E(!0);try{await ae(),d("Requested Arr rebuild","success"),u()}catch(a){d(a instanceof Error?a.message:"Failed to rebuild Arrs","error")}finally{E(!1)}}})},[u,d]),V=l.useMemo(()=>{const a=new Map,c=r=>{const o=(r.category??"").toLowerCase(),i=(r.name??"").toLowerCase();return o.includes("radarr")||i.includes("radarr")?"Radarr":o.includes("sonarr")||i.includes("sonarr")?"Sonarr":o.includes("lidarr")||i.includes("lidarr")?"Lidarr":o.includes("qbit")||o.includes("qbittorrent")||i.includes("qbit")||i.includes("qbittorrent")?"qBittorrent":"Other"},x=T?.arrs??[],A=x.some(r=>r.type==="radarr"),b=x.some(r=>r.type==="sonarr"),g=x.some(r=>r.type==="lidarr");n.forEach(r=>{const o=c(r);if(o==="Radarr"&&!A||o==="Sonarr"&&!b||o==="Lidarr"&&!g)return;a.has(o)||a.set(o,new Map);const i=a.get(o),f=r.name||r.category||`${r.category}:${r.kind}`;i.has(f)||i.set(f,[]),i.get(f).push(r)});const v=["Radarr","Sonarr","Lidarr","qBittorrent","Other"],_=Array.from(a.entries()).map(([r,o])=>{const i=Array.from(o.entries()).map(([f,R])=>({name:f,items:R.sort((I,M)=>I.kind.localeCompare(M.kind))})).sort((f,R)=>f.name.localeCompare(R.name));return{app:r,instances:i}}).filter(r=>r.instances.length);return _.sort((r,o)=>{const i=f=>{const R=v.indexOf(f);return R===-1?Number.MAX_SAFE_INTEGER:R};return i(r.app)-i(o.app)||r.app.localeCompare(o.app)}),_},[n,T]),J=l.useCallback(async a=>{try{await Promise.all(a.map(c=>K(c.category,c.kind))),d(`Restarted ${a[0]?.name??"group"}`,"success"),u()}catch(c){d(c instanceof Error?c.message:"Failed to restart process group","error")}},[u,d]),F=V.map(({app:a,instances:c})=>{const x=c.map(({name:A,items:b})=>{const g=a==="qBittorrent"?z.filter(s=>{const p=A.toLowerCase(),m=s.instance.toLowerCase();return p===m||p.endsWith(`-${m}`)||p.endsWith(`_${m}`)}):[],v=A,_=b.filter(s=>s.alive).length,r=b.length,o=r===0?"":_===r?"status-indicator--ok":_===0?"status-indicator--bad":"",i=["status-indicator"];o&&i.push(o);const f=r===0?"No processes":_===r?"All running":_===0?"Stopped":`${_}/${r} running`,R=r===1?"1 process":`${r} processes`,I=v==="FreeSpaceManager"?"Free Space Manager":v,O=Array.from(new Set(b.map(s=>s.kind))).filter(s=>{const p=s.toLowerCase();return p!=="search"&&p!=="torrent"}),D=s=>s&&s.charAt(0).toUpperCase()+s.slice(1);return e.jsxs("div",{className:"process-card",children:[e.jsxs("div",{className:"process-card__header",children:[e.jsxs("div",{className:"process-card__title",children:[e.jsx("div",{className:"process-card__name",children:I}),e.jsx("div",{className:"process-card__summary",children:R}),O.length?e.jsx("div",{className:"process-card__badges",children:O.map(s=>e.jsx("span",{className:"process-card__badge",children:D(s)},`${v}:${s}:badge`))}):null]}),e.jsx("div",{className:i.join(" "),title:f})]}),e.jsxs("div",{className:"process-card__list",children:[b.map(s=>e.jsxs("div",{className:"process-chip",children:[e.jsxs("div",{className:"process-chip__top",children:[e.jsx("div",{className:"process-chip__name",children:D(s.kind)}),e.jsx("div",{className:`status-pill__dot ${s.alive?"text-success":"text-danger"}`})]}),e.jsx("div",{className:"process-chip__detail",children:(()=>{if(s.rebuilding)return"Rebuilding";const p=s.kind.toLowerCase();if(p==="search")return(s.searchSummary??"")||"No searches recorded";if(p==="category"){const m=typeof s.categoryCount=="number"?s.categoryCount:null;return m!==null?`Managing ${m} ${m===1?"category":"categories"}`:"Category manager"}if(p==="torrent"){const m=s.metricType?.toLowerCase(),w=typeof s.categoryCount=="number"?s.categoryCount:null,L=typeof s.queueCount=="number"?s.queueCount:null;return m?m==="category"&&w!==null?`Torrent count ${w}`:m==="free-space"&&L!==null?`Torrent count ${L}`:"Torrent count unavailable":`Torrents in queue ${L!==null?L:"?"} / total ${w!==null?w:"?"}`}return""})()}),e.jsx("div",{className:"process-chip__actions",children:e.jsx("button",{className:"btn small",onClick:()=>Q(s.category,s.kind),children:"Restart"})})]},`${s.category}:${s.kind}`)),g.map(s=>{const p=T?.qbitInstances?.[s.instance]?.alive??!1,m=s.seedingCount>0?`${s.torrentCount} torrents (${s.seedingCount} seeding)`:`${s.torrentCount} torrents`;return e.jsxs("div",{className:"process-chip process-chip--info",children:[e.jsxs("div",{className:"process-chip__top",children:[e.jsxs("div",{className:"process-chip__name",children:[s.category,e.jsx("span",{className:"process-chip__managed-badge",children:s.managedBy==="arr"?"Arr":"qBit"})]}),e.jsx("div",{className:`status-pill__dot ${p?"text-success":"text-danger"}`})]}),e.jsx("div",{className:"process-chip__detail",children:m})]},`cat:${s.instance}:${s.category}`)})]}),e.jsx("div",{className:"process-card__footer",children:e.jsx("button",{className:"btn small",onClick:()=>{J(b)},children:"Restart All"})})]},v)});return{app:a,cards:x}});return e.jsxs(e.Fragment,{children:[e.jsxs("section",{className:"card",children:[e.jsx("div",{className:"card-header",children:"Processes"}),e.jsxs("div",{className:"card-body stack",children:[e.jsx("div",{className:"row",children:e.jsxs("div",{className:"col inline",children:[e.jsxs("button",{className:"btn ghost",onClick:()=>{u()},disabled:y,children:[y&&e.jsx("span",{className:"spinner"}),e.jsx(q,{src:B}),y?"Refreshing...":"Refresh"]}),e.jsxs("button",{className:"btn",onClick:()=>{H()},disabled:j,children:[j&&e.jsx("span",{className:"spinner"}),e.jsx(q,{src:B}),j?"Restarting...":"Restart All"]}),e.jsxs("button",{className:"btn",onClick:()=>{U()},disabled:N,children:[N&&e.jsx("span",{className:"spinner"}),e.jsx(q,{src:oe}),N?"Rebuilding...":"Rebuild Arrs"]})]})}),F.length?F.map(({app:a,cards:c})=>e.jsxs("div",{className:"process-section",children:[e.jsx("div",{className:"process-section__title",children:a}),e.jsx("div",{className:"process-grid",children:c})]},a)):e.jsx("div",{className:"empty-state",children:"No processes available."})]})]}),k&&e.jsx(ce,{title:k.title,message:k.message,confirmLabel:"Confirm",cancelLabel:"Cancel",danger:!0,onConfirm:k.onConfirm,onCancel:()=>S(null)})]})}export{je as ProcessesView};
|
|
2
2
|
//# sourceMappingURL=ProcessesView.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProcessesView.js","sources":["../../../webui/src/components/ConfirmDialog.tsx","../../../webui/src/icons/build.svg","../../../webui/src/pages/ProcessesView.tsx"],"sourcesContent":["import type { JSX } from \"react\";\nimport { IconImage } from \"./IconImage\";\nimport CloseIcon from \"../icons/close.svg\";\n\ninterface ConfirmDialogProps {\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n onConfirm: () => void;\n onCancel: () => void;\n danger?: boolean;\n}\n\nexport function ConfirmDialog({\n title,\n message,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n danger = false,\n}: ConfirmDialogProps): JSX.Element {\n return (\n <div className=\"modal-backdrop\" onClick={onCancel}>\n <div\n className=\"modal\"\n style={{ maxWidth: '500px' }}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"modal-header\">\n <h2>{title}</h2>\n <button className=\"btn ghost\" onClick={onCancel}>\n <IconImage src={CloseIcon} />\n </button>\n </div>\n <div className=\"modal-body\">\n <p style={{ margin: 0, lineHeight: 1.6 }}>{message}</p>\n </div>\n <div className=\"modal-footer\">\n <button className=\"btn ghost\" onClick={onCancel}>\n {cancelLabel}\n </button>\n <button\n className={`btn ${danger ? 'danger' : 'primary'}`}\n onClick={onConfirm}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </div>\n );\n}\n","export default \"__VITE_ASSET__DznMzWc1__\"","import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport {\n getProcesses,\n getStatus,\n rebuildArrs,\n restartAllProcesses,\n restartProcess,\n} from \"../api/client\";\nimport type { ProcessInfo, StatusResponse } from \"../api/types\";\nimport { useToast } from \"../context/ToastContext\";\nimport { useInterval } from \"../hooks/useInterval\";\nimport { IconImage } from \"../components/IconImage\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\n\nimport RefreshIcon from \"../icons/refresh-arrow.svg\";\nimport RestartIcon from \"../icons/refresh-arrow.svg\";\nimport ToolsIcon from \"../icons/build.svg\";\n\nconst RELEASE_TOKEN_REGEX =\n /\\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\\b/i;\nconst EPISODE_TOKEN_REGEX = /\\bS\\d{1,3}E\\d{1,3}\\b/i;\nconst SEASON_TOKEN_REGEX = /\\bSeason\\s+\\d+\\b/i;\n\nfunction sanitizeSearchSummary(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return \"\";\n\n // Keep \"X queued items\" messages as-is (don't filter them out)\n if (/^\\d+\\s+queued item/i.test(trimmed)) {\n return trimmed;\n }\n\n const normalized = trimmed.replace(/\\s+/g, \" \");\n const releaseMatch = normalized.match(\n /^(?<title>.+?)\\s+(?<year>(?:19|20)\\d{2})(?:\\s+(?<rest>.*))?$/\n );\n\n if (releaseMatch) {\n const rest = releaseMatch.groups?.rest ?? \"\";\n const looksLikeEpisode =\n EPISODE_TOKEN_REGEX.test(rest) || SEASON_TOKEN_REGEX.test(rest);\n if (rest && !looksLikeEpisode && RELEASE_TOKEN_REGEX.test(rest)) {\n const rawTitle = releaseMatch.groups?.title ?? \"\";\n const cleanedTitle = rawTitle\n .replace(/[-_.]/g, \" \")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n const year = releaseMatch.groups?.year ?? \"\";\n if (cleanedTitle) {\n return year ? `${cleanedTitle} (${year})` : cleanedTitle;\n }\n }\n }\n\n return normalized;\n}\n\nfunction isProcessEqual(a: ProcessInfo, b: ProcessInfo): boolean {\n return (\n a.category === b.category &&\n a.name === b.name &&\n a.kind === b.kind &&\n a.pid === b.pid &&\n a.alive === b.alive &&\n (a.rebuilding ?? false) === (b.rebuilding ?? false) &&\n (a.searchSummary ?? \"\") === (b.searchSummary ?? \"\") &&\n (a.searchTimestamp ?? \"\") === (b.searchTimestamp ?? \"\") &&\n (a.queueCount ?? null) === (b.queueCount ?? null) &&\n (a.categoryCount ?? null) === (b.categoryCount ?? null) &&\n (a.metricType ?? \"\") === (b.metricType ?? \"\")\n );\n}\n\nfunction areProcessListsEqual(a: ProcessInfo[], b: ProcessInfo[]): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (!isProcessEqual(a[index], b[index])) {\n return false;\n }\n }\n return true;\n}\n\nfunction getRefreshDelay(active: boolean, processes: ProcessInfo[]): number | null {\n if (!active) return null;\n // Refresh every 1 second when active\n return 1000;\n}\n\ninterface ProcessesViewProps {\n active: boolean;\n}\n\nexport function ProcessesView({ active }: ProcessesViewProps): JSX.Element {\n const [processes, setProcesses] = useState<ProcessInfo[]>([]);\n const [loading, setLoading] = useState(false);\n const [restartingAll, setRestartingAll] = useState(false);\n const [rebuildingArrs, setRebuildingArrs] = useState(false);\n const [statusData, setStatusData] = useState<StatusResponse | null>(null);\n const [confirmAction, setConfirmAction] = useState<{\n title: string;\n message: string;\n onConfirm: () => void;\n } | null>(null);\n const { push } = useToast();\n const isFetching = useRef(false);\n\n const load = useCallback(async (showLoading = true) => {\n if (isFetching.current) {\n return;\n }\n isFetching.current = true;\n if (showLoading) {\n setLoading(true);\n }\n try {\n const [processData, status] = await Promise.all([\n getProcesses(),\n getStatus(),\n ]);\n const next = (processData.processes ?? []).map((process) => {\n if (typeof process.searchSummary === \"string\") {\n const sanitized = sanitizeSearchSummary(process.searchSummary);\n return {\n ...process,\n searchSummary: sanitized,\n };\n }\n return process;\n });\n setProcesses((prev) =>\n areProcessListsEqual(prev, next) ? prev : next\n );\n setStatusData(status);\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to load processes list\",\n \"error\"\n );\n } finally {\n isFetching.current = false;\n if (showLoading) {\n setLoading(false);\n }\n }\n }, [push]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (active) {\n void load();\n }\n }, [active, load]);\n\n const refreshDelay = useMemo(\n () => getRefreshDelay(active, processes),\n [active, processes]\n );\n\n useInterval(() => {\n void load(false); // Auto-refresh without showing loading spinner\n }, refreshDelay);\n\n const handleRestart = useCallback(\n async (category: string, kind: string) => {\n try {\n await restartProcess(category, kind);\n push(`Restarted ${category}:${kind}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : `Failed to restart ${category}:${kind}`,\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const handleRestartAll = useCallback(async () => {\n setConfirmAction({\n title: \"Restart All Processes\",\n message: \"Are you sure you want to restart all processes? This will temporarily interrupt all operations.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRestartingAll(true);\n try {\n await restartAllProcesses();\n push(\"Restarted all processes\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to restart all\",\n \"error\"\n );\n } finally {\n setRestartingAll(false);\n }\n }\n });\n }, [load, push]);\n\n const handleRebuildArrs = useCallback(async () => {\n setConfirmAction({\n title: \"Rebuild Arrs\",\n message: \"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRebuildingArrs(true);\n try {\n await rebuildArrs();\n push(\"Requested Arr rebuild\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to rebuild Arrs\",\n \"error\"\n );\n } finally {\n setRebuildingArrs(false);\n }\n }\n });\n }, [load, push]);\n\n const groupedProcesses = useMemo(() => {\n interface Instance {\n name: string;\n items: ProcessInfo[];\n }\n interface AppGroup {\n app: string;\n instances: Instance[];\n }\n const appBuckets = new Map<string, Map<string, ProcessInfo[]>>();\n\n const classifyApp = (proc: ProcessInfo): string => {\n const category = (proc.category ?? \"\").toLowerCase();\n const name = (proc.name ?? \"\").toLowerCase();\n if (category.includes(\"radarr\") || name.includes(\"radarr\")) return \"Radarr\";\n if (category.includes(\"sonarr\") || name.includes(\"sonarr\")) return \"Sonarr\";\n if (category.includes(\"lidarr\") || name.includes(\"lidarr\")) return \"Lidarr\";\n if (\n category.includes(\"qbit\") ||\n category.includes(\"qbittorrent\") ||\n name.includes(\"qbit\") ||\n name.includes(\"qbittorrent\")\n ) {\n return \"qBittorrent\";\n }\n return \"Other\";\n };\n\n // Check which Arr types are configured\n const arrs = statusData?.arrs ?? [];\n const hasRadarr = arrs.some((arr) => arr.type === \"radarr\");\n const hasSonarr = arrs.some((arr) => arr.type === \"sonarr\");\n const hasLidarr = arrs.some((arr) => arr.type === \"lidarr\");\n\n processes.forEach((proc) => {\n const app = classifyApp(proc);\n\n // Skip Arr processes if that Arr type is not configured\n if (app === \"Radarr\" && !hasRadarr) return;\n if (app === \"Sonarr\" && !hasSonarr) return;\n if (app === \"Lidarr\" && !hasLidarr) return;\n\n if (!appBuckets.has(app)) appBuckets.set(app, new Map());\n const instances = appBuckets.get(app)!;\n const instanceKey =\n proc.name || proc.category || `${proc.category}:${proc.kind}`;\n if (!instances.has(instanceKey)) instances.set(instanceKey, []);\n instances.get(instanceKey)!.push(proc);\n });\n\n const appOrder = [\"Radarr\", \"Sonarr\", \"Lidarr\", \"qBittorrent\", \"Other\"];\n\n const result: AppGroup[] = Array.from(appBuckets.entries())\n .map(([app, instances]) => {\n const sortedInstances = Array.from(instances.entries())\n .map(([name, items]) => ({\n name,\n items: items.sort((a, b) => a.kind.localeCompare(b.kind)),\n }))\n .sort((a, b) => a.name.localeCompare(b.name));\n return { app, instances: sortedInstances };\n })\n .filter((group) => group.instances.length);\n\n result.sort((a, b) => {\n const order = (label: string) => {\n const index = appOrder.indexOf(label);\n return index === -1 ? Number.MAX_SAFE_INTEGER : index;\n };\n return order(a.app) - order(b.app) || a.app.localeCompare(b.app);\n });\n\n return result;\n }, [processes, statusData]);\n\n const handleRestartGroup = useCallback(\n async (items: ProcessInfo[]) => {\n try {\n await Promise.all(\n items.map((item) => restartProcess(item.category, item.kind))\n );\n push(`Restarted ${items[0]?.name ?? \"group\"}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to restart process group\",\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const cardsByApp = groupedProcesses.map(({ app, instances }) => {\n const cards = instances.map(({ name, items }) => {\n const runningCount = items.filter((item) => item.alive).length;\n const totalCount = items.length;\n const tone =\n totalCount === 0\n ? \"\"\n : runningCount === totalCount\n ? \"status-indicator--ok\"\n : runningCount === 0\n ? \"status-indicator--bad\"\n : \"\";\n const statusClass = [\"status-indicator\"];\n if (tone) statusClass.push(tone);\n const statusLabel =\n totalCount === 0\n ? \"No processes\"\n : runningCount === totalCount\n ? \"All running\"\n : runningCount === 0\n ? \"Stopped\"\n : `${runningCount}/${totalCount} running`;\n const summaryLabel = totalCount === 1 ? \"1 process\" : `${totalCount} processes`;\n const displayName = name === \"FreeSpaceManager\" ? \"Free Space Manager\" : name;\n const uniqueKinds = Array.from(new Set(items.map((item) => item.kind)));\n const filteredKinds = uniqueKinds.filter((kind) => {\n const lower = kind.toLowerCase();\n return lower !== \"search\" && lower !== \"torrent\";\n });\n const formatKind = (kind: string) =>\n kind ? kind.charAt(0).toUpperCase() + kind.slice(1) : kind;\n\n return (\n <div className=\"process-card\" key={name}>\n <div className=\"process-card__header\">\n <div className=\"process-card__title\">\n <div className=\"process-card__name\">{displayName}</div>\n <div className=\"process-card__summary\">{summaryLabel}</div>\n {filteredKinds.length ? (\n <div className=\"process-card__badges\">\n {filteredKinds.map((kind) => (\n <span key={`${name}:${kind}:badge`} className=\"process-card__badge\">\n {formatKind(kind)}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n <div className={statusClass.join(\" \")} title={statusLabel} />\n </div>\n <div className=\"process-card__list\">\n {items.map((item) => (\n <div className=\"process-chip\" key={`${item.category}:${item.kind}`}>\n <div className=\"process-chip__top\">\n <div className=\"process-chip__name\">{formatKind(item.kind)}</div>\n <div className={`status-pill__dot ${item.alive ? \"text-success\" : \"text-danger\"}`} />\n </div>\n <div className=\"process-chip__detail\">\n {(() => {\n if (item.rebuilding) {\n return \"Rebuilding\";\n }\n const kindLower = item.kind.toLowerCase();\n if (kindLower === \"search\") {\n const summary = item.searchSummary ?? \"\";\n return summary || \"No searches recorded\";\n }\n if (kindLower === \"torrent\") {\n const metricType = item.metricType?.toLowerCase();\n const categoryTotal =\n typeof item.categoryCount === \"number\" ? item.categoryCount : null;\n const queueTotal =\n typeof item.queueCount === \"number\" ? item.queueCount : null;\n\n if (!metricType) {\n const queueLabel = queueTotal !== null ? queueTotal : \"?\";\n const categoryLabel = categoryTotal !== null ? categoryTotal : \"?\";\n return `Torrents in queue ${queueLabel} / total ${categoryLabel}`;\n }\n\n if (metricType === \"category\" && categoryTotal !== null) {\n return `Torrent count ${categoryTotal}`;\n }\n\n if (metricType === \"free-space\" && queueTotal !== null) {\n return `Torrent count ${queueTotal}`;\n }\n\n return \"Torrent count unavailable\";\n }\n return \"\";\n })()}\n </div>\n <div className=\"process-chip__actions\">\n <button\n className=\"btn small\"\n onClick={() => handleRestart(item.category, item.kind)}\n >\n Restart\n </button>\n </div>\n </div>\n ))}\n </div>\n <div className=\"process-card__footer\">\n <button\n className=\"btn small outline\"\n onClick={() => void handleRestartGroup(items)}\n >\n Restart All\n </button>\n </div>\n </div>\n );\n });\n return { app, cards };\n });\n\n return (\n <>\n <section className=\"card\">\n <div className=\"card-header\">Processes</div>\n <div className=\"card-body stack\">\n <div className=\"row\">\n <div className=\"col inline\">\n <button className=\"btn ghost\" onClick={() => void load()} disabled={loading}>\n {loading && <span className=\"spinner\" />}\n <IconImage src={RefreshIcon} />\n {loading ? 'Refreshing...' : 'Refresh'}\n </button>\n <button className=\"btn\" onClick={() => void handleRestartAll()} disabled={restartingAll}>\n {restartingAll && <span className=\"spinner\" />}\n <IconImage src={RestartIcon} />\n {restartingAll ? 'Restarting...' : 'Restart All'}\n </button>\n <button className=\"btn\" onClick={() => void handleRebuildArrs()} disabled={rebuildingArrs}>\n {rebuildingArrs && <span className=\"spinner\" />}\n <IconImage src={ToolsIcon} />\n {rebuildingArrs ? 'Rebuilding...' : 'Rebuild Arrs'}\n </button>\n </div>\n </div>\n {cardsByApp.length ? (\n cardsByApp.map(({ app, cards }) => (\n <div className=\"process-section\" key={app}>\n <div className=\"process-section__title\">{app}</div>\n <div className=\"process-grid\">{cards}</div>\n </div>\n ))\n ) : (\n <div className=\"empty-state\">No processes available.</div>\n )}\n </div>\n </section>\n {confirmAction && (\n <ConfirmDialog\n title={confirmAction.title}\n message={confirmAction.message}\n confirmLabel=\"Confirm\"\n cancelLabel=\"Cancel\"\n danger={true}\n onConfirm={confirmAction.onConfirm}\n onCancel={() => setConfirmAction(null)}\n />\n )}\n </>\n );\n}\n"],"names":["ConfirmDialog","title","message","confirmLabel","cancelLabel","onConfirm","onCancel","danger","jsx","jsxs","e","IconImage","CloseIcon","ToolsIcon","RELEASE_TOKEN_REGEX","EPISODE_TOKEN_REGEX","SEASON_TOKEN_REGEX","sanitizeSearchSummary","raw","trimmed","normalized","releaseMatch","rest","looksLikeEpisode","cleanedTitle","year","isProcessEqual","a","b","areProcessListsEqual","index","getRefreshDelay","active","processes","ProcessesView","setProcesses","useState","loading","setLoading","restartingAll","setRestartingAll","rebuildingArrs","setRebuildingArrs","statusData","setStatusData","confirmAction","setConfirmAction","push","useToast","isFetching","useRef","load","useCallback","showLoading","processData","status","getProcesses","getStatus","next","process","sanitized","prev","error","useEffect","refreshDelay","useMemo","useInterval","handleRestart","category","kind","restartProcess","handleRestartAll","restartAllProcesses","handleRebuildArrs","rebuildArrs","groupedProcesses","appBuckets","classifyApp","proc","name","arrs","hasRadarr","arr","hasSonarr","hasLidarr","app","instances","instanceKey","appOrder","result","sortedInstances","items","group","order","label","handleRestartGroup","item","cardsByApp","cards","runningCount","totalCount","tone","statusClass","statusLabel","summaryLabel","displayName","filteredKinds","lower","formatKind","kindLower","metricType","categoryTotal","queueTotal","Fragment","RefreshIcon","RestartIcon"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProcessesView.js","sources":["../../../webui/src/components/ConfirmDialog.tsx","../../../webui/src/icons/build.svg","../../../webui/src/pages/ProcessesView.tsx"],"sourcesContent":["import type { JSX } from \"react\";\nimport { IconImage } from \"./IconImage\";\nimport CloseIcon from \"../icons/close.svg\";\n\ninterface ConfirmDialogProps {\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n onConfirm: () => void;\n onCancel: () => void;\n danger?: boolean;\n}\n\nexport function ConfirmDialog({\n title,\n message,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n onConfirm,\n onCancel,\n danger = false,\n}: ConfirmDialogProps): JSX.Element {\n return (\n <div className=\"modal-backdrop\" onClick={onCancel}>\n <div\n className=\"modal\"\n style={{ maxWidth: '500px' }}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"modal-header\">\n <h2>{title}</h2>\n <button className=\"btn ghost\" onClick={onCancel}>\n <IconImage src={CloseIcon} />\n </button>\n </div>\n <div className=\"modal-body\">\n <p style={{ margin: 0, lineHeight: 1.6 }}>{message}</p>\n </div>\n <div className=\"modal-footer\">\n <button className=\"btn ghost\" onClick={onCancel}>\n {cancelLabel}\n </button>\n <button\n className={`btn ${danger ? 'danger' : 'primary'}`}\n onClick={onConfirm}\n >\n {confirmLabel}\n </button>\n </div>\n </div>\n </div>\n );\n}\n","export default \"__VITE_ASSET__DznMzWc1__\"","import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport {\n getProcesses,\n getQbitCategories,\n getStatus,\n rebuildArrs,\n restartAllProcesses,\n restartProcess,\n} from \"../api/client\";\nimport type { ProcessInfo, QbitCategory, StatusResponse } from \"../api/types\";\nimport { useToast } from \"../context/ToastContext\";\nimport { useInterval } from \"../hooks/useInterval\";\nimport { IconImage } from \"../components/IconImage\";\nimport { ConfirmDialog } from \"../components/ConfirmDialog\";\n\nimport RefreshIcon from \"../icons/refresh-arrow.svg\";\nimport RestartIcon from \"../icons/refresh-arrow.svg\";\nimport ToolsIcon from \"../icons/build.svg\";\n\nconst RELEASE_TOKEN_REGEX =\n /\\b(480p|576p|720p|1080p|2160p|4k|8k|web[-_. ]?(?:dl|rip)|hdrip|hdtv|bluray|bd(?:rip)?|brrip|webrip|remux|x264|x265|hevc|dts|truehd|atmos|proper|repack|dvdrip|hdr|amzn|nf)\\b/i;\nconst EPISODE_TOKEN_REGEX = /\\bS\\d{1,3}E\\d{1,3}\\b/i;\nconst SEASON_TOKEN_REGEX = /\\bSeason\\s+\\d+\\b/i;\n\nfunction sanitizeSearchSummary(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return \"\";\n\n // Keep \"X queued items\" messages as-is (don't filter them out)\n if (/^\\d+\\s+queued item/i.test(trimmed)) {\n return trimmed;\n }\n\n const normalized = trimmed.replace(/\\s+/g, \" \");\n const releaseMatch = normalized.match(\n /^(?<title>.+?)\\s+(?<year>(?:19|20)\\d{2})(?:\\s+(?<rest>.*))?$/\n );\n\n if (releaseMatch) {\n const rest = releaseMatch.groups?.rest ?? \"\";\n const looksLikeEpisode =\n EPISODE_TOKEN_REGEX.test(rest) || SEASON_TOKEN_REGEX.test(rest);\n if (rest && !looksLikeEpisode && RELEASE_TOKEN_REGEX.test(rest)) {\n const rawTitle = releaseMatch.groups?.title ?? \"\";\n const cleanedTitle = rawTitle\n .replace(/[-_.]/g, \" \")\n .replace(/\\s{2,}/g, \" \")\n .trim();\n const year = releaseMatch.groups?.year ?? \"\";\n if (cleanedTitle) {\n return year ? `${cleanedTitle} (${year})` : cleanedTitle;\n }\n }\n }\n\n return normalized;\n}\n\nfunction isProcessEqual(a: ProcessInfo, b: ProcessInfo): boolean {\n return (\n a.category === b.category &&\n a.name === b.name &&\n a.kind === b.kind &&\n a.pid === b.pid &&\n a.alive === b.alive &&\n (a.rebuilding ?? false) === (b.rebuilding ?? false) &&\n (a.searchSummary ?? \"\") === (b.searchSummary ?? \"\") &&\n (a.searchTimestamp ?? \"\") === (b.searchTimestamp ?? \"\") &&\n (a.queueCount ?? null) === (b.queueCount ?? null) &&\n (a.categoryCount ?? null) === (b.categoryCount ?? null) &&\n (a.metricType ?? \"\") === (b.metricType ?? \"\")\n );\n}\n\nfunction areProcessListsEqual(a: ProcessInfo[], b: ProcessInfo[]): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (!isProcessEqual(a[index], b[index])) {\n return false;\n }\n }\n return true;\n}\n\nfunction getRefreshDelay(active: boolean): number | null {\n if (!active) return null;\n // Refresh every 1 second when active\n return 1000;\n}\n\ninterface ProcessesViewProps {\n active: boolean;\n}\n\nexport function ProcessesView({ active }: ProcessesViewProps): JSX.Element {\n const [processes, setProcesses] = useState<ProcessInfo[]>([]);\n const [loading, setLoading] = useState(false);\n const [restartingAll, setRestartingAll] = useState(false);\n const [rebuildingArrs, setRebuildingArrs] = useState(false);\n const [statusData, setStatusData] = useState<StatusResponse | null>(null);\n const [qbitCategories, setQbitCategories] = useState<QbitCategory[]>([]);\n const [confirmAction, setConfirmAction] = useState<{\n title: string;\n message: string;\n onConfirm: () => void;\n } | null>(null);\n const { push } = useToast();\n const isFetching = useRef(false);\n\n const load = useCallback(async (showLoading = true) => {\n if (isFetching.current) {\n return;\n }\n isFetching.current = true;\n if (showLoading) {\n setLoading(true);\n }\n try {\n const [processData, status, categoriesData] = await Promise.all([\n getProcesses(),\n getStatus(),\n getQbitCategories().catch(() => null),\n ]);\n const next = (processData.processes ?? []).map((process) => {\n if (typeof process.searchSummary === \"string\") {\n const sanitized = sanitizeSearchSummary(process.searchSummary);\n return {\n ...process,\n searchSummary: sanitized,\n };\n }\n return process;\n });\n setProcesses((prev) =>\n areProcessListsEqual(prev, next) ? prev : next\n );\n setStatusData(status);\n if (categoriesData?.categories) {\n setQbitCategories(categoriesData.categories);\n }\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to load processes list\",\n \"error\"\n );\n } finally {\n isFetching.current = false;\n if (showLoading) {\n setLoading(false);\n }\n }\n }, [push]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n useEffect(() => {\n if (active) {\n void load();\n }\n }, [active, load]);\n\n const refreshDelay = useMemo(\n () => getRefreshDelay(active),\n [active]\n );\n\n useInterval(() => {\n void load(false); // Auto-refresh without showing loading spinner\n }, refreshDelay);\n\n const handleRestart = useCallback(\n async (category: string, kind: string) => {\n try {\n await restartProcess(category, kind);\n push(`Restarted ${category}:${kind}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : `Failed to restart ${category}:${kind}`,\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const handleRestartAll = useCallback(async () => {\n setConfirmAction({\n title: \"Restart All Processes\",\n message: \"Are you sure you want to restart all processes? This will temporarily interrupt all operations.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRestartingAll(true);\n try {\n await restartAllProcesses();\n push(\"Restarted all processes\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to restart all\",\n \"error\"\n );\n } finally {\n setRestartingAll(false);\n }\n }\n });\n }, [load, push]);\n\n const handleRebuildArrs = useCallback(async () => {\n setConfirmAction({\n title: \"Rebuild Arrs\",\n message: \"Are you sure you want to rebuild all Arr instances? This will refresh all connections and may take some time.\",\n onConfirm: async () => {\n setConfirmAction(null);\n setRebuildingArrs(true);\n try {\n await rebuildArrs();\n push(\"Requested Arr rebuild\", \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error ? error.message : \"Failed to rebuild Arrs\",\n \"error\"\n );\n } finally {\n setRebuildingArrs(false);\n }\n }\n });\n }, [load, push]);\n\n const groupedProcesses = useMemo(() => {\n interface Instance {\n name: string;\n items: ProcessInfo[];\n }\n interface AppGroup {\n app: string;\n instances: Instance[];\n }\n const appBuckets = new Map<string, Map<string, ProcessInfo[]>>();\n\n const classifyApp = (proc: ProcessInfo): string => {\n const category = (proc.category ?? \"\").toLowerCase();\n const name = (proc.name ?? \"\").toLowerCase();\n if (category.includes(\"radarr\") || name.includes(\"radarr\")) return \"Radarr\";\n if (category.includes(\"sonarr\") || name.includes(\"sonarr\")) return \"Sonarr\";\n if (category.includes(\"lidarr\") || name.includes(\"lidarr\")) return \"Lidarr\";\n if (\n category.includes(\"qbit\") ||\n category.includes(\"qbittorrent\") ||\n name.includes(\"qbit\") ||\n name.includes(\"qbittorrent\")\n ) {\n return \"qBittorrent\";\n }\n return \"Other\";\n };\n\n // Check which Arr types are configured\n const arrs = statusData?.arrs ?? [];\n const hasRadarr = arrs.some((arr) => arr.type === \"radarr\");\n const hasSonarr = arrs.some((arr) => arr.type === \"sonarr\");\n const hasLidarr = arrs.some((arr) => arr.type === \"lidarr\");\n\n processes.forEach((proc) => {\n const app = classifyApp(proc);\n\n // Skip Arr processes if that Arr type is not configured\n if (app === \"Radarr\" && !hasRadarr) return;\n if (app === \"Sonarr\" && !hasSonarr) return;\n if (app === \"Lidarr\" && !hasLidarr) return;\n\n if (!appBuckets.has(app)) appBuckets.set(app, new Map());\n const instances = appBuckets.get(app)!;\n const instanceKey =\n proc.name || proc.category || `${proc.category}:${proc.kind}`;\n if (!instances.has(instanceKey)) instances.set(instanceKey, []);\n instances.get(instanceKey)!.push(proc);\n });\n\n const appOrder = [\"Radarr\", \"Sonarr\", \"Lidarr\", \"qBittorrent\", \"Other\"];\n\n const result: AppGroup[] = Array.from(appBuckets.entries())\n .map(([app, instances]) => {\n const sortedInstances = Array.from(instances.entries())\n .map(([name, items]) => ({\n name,\n items: items.sort((a, b) => a.kind.localeCompare(b.kind)),\n }))\n .sort((a, b) => a.name.localeCompare(b.name));\n return { app, instances: sortedInstances };\n })\n .filter((group) => group.instances.length);\n\n result.sort((a, b) => {\n const order = (label: string) => {\n const index = appOrder.indexOf(label);\n return index === -1 ? Number.MAX_SAFE_INTEGER : index;\n };\n return order(a.app) - order(b.app) || a.app.localeCompare(b.app);\n });\n\n return result;\n }, [processes, statusData]);\n\n const handleRestartGroup = useCallback(\n async (items: ProcessInfo[]) => {\n try {\n await Promise.all(\n items.map((item) => restartProcess(item.category, item.kind))\n );\n push(`Restarted ${items[0]?.name ?? \"group\"}`, \"success\");\n void load();\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to restart process group\",\n \"error\"\n );\n }\n },\n [load, push]\n );\n\n const cardsByApp = groupedProcesses.map(({ app, instances }) => {\n const cards = instances.map(({ name: instanceName, items }) => {\n // For qBittorrent cards, find matching category data\n const instanceCategories = app === \"qBittorrent\"\n ? qbitCategories.filter((cat) => {\n // Match instance name: card name like \"qBit-main\" -> instance \"main\"\n const nameLower = instanceName.toLowerCase();\n const instLower = cat.instance.toLowerCase();\n return nameLower === instLower\n || nameLower.endsWith(`-${instLower}`)\n || nameLower.endsWith(`_${instLower}`);\n })\n : [];\n const name = instanceName;\n const runningCount = items.filter((item) => item.alive).length;\n const totalCount = items.length;\n const tone =\n totalCount === 0\n ? \"\"\n : runningCount === totalCount\n ? \"status-indicator--ok\"\n : runningCount === 0\n ? \"status-indicator--bad\"\n : \"\";\n const statusClass = [\"status-indicator\"];\n if (tone) statusClass.push(tone);\n const statusLabel =\n totalCount === 0\n ? \"No processes\"\n : runningCount === totalCount\n ? \"All running\"\n : runningCount === 0\n ? \"Stopped\"\n : `${runningCount}/${totalCount} running`;\n const summaryLabel = totalCount === 1 ? \"1 process\" : `${totalCount} processes`;\n const displayName = name === \"FreeSpaceManager\" ? \"Free Space Manager\" : name;\n const uniqueKinds = Array.from(new Set(items.map((item) => item.kind)));\n const filteredKinds = uniqueKinds.filter((kind) => {\n const lower = kind.toLowerCase();\n return lower !== \"search\" && lower !== \"torrent\";\n });\n const formatKind = (kind: string) =>\n kind ? kind.charAt(0).toUpperCase() + kind.slice(1) : kind;\n\n return (\n <div className=\"process-card\" key={name}>\n <div className=\"process-card__header\">\n <div className=\"process-card__title\">\n <div className=\"process-card__name\">{displayName}</div>\n <div className=\"process-card__summary\">{summaryLabel}</div>\n {filteredKinds.length ? (\n <div className=\"process-card__badges\">\n {filteredKinds.map((kind) => (\n <span key={`${name}:${kind}:badge`} className=\"process-card__badge\">\n {formatKind(kind)}\n </span>\n ))}\n </div>\n ) : null}\n </div>\n <div className={statusClass.join(\" \")} title={statusLabel} />\n </div>\n <div className=\"process-card__list\">\n {items.map((item) => (\n <div className=\"process-chip\" key={`${item.category}:${item.kind}`}>\n <div className=\"process-chip__top\">\n <div className=\"process-chip__name\">{formatKind(item.kind)}</div>\n <div className={`status-pill__dot ${item.alive ? \"text-success\" : \"text-danger\"}`} />\n </div>\n <div className=\"process-chip__detail\">\n {(() => {\n if (item.rebuilding) {\n return \"Rebuilding\";\n }\n const kindLower = item.kind.toLowerCase();\n if (kindLower === \"search\") {\n const summary = item.searchSummary ?? \"\";\n return summary || \"No searches recorded\";\n }\n if (kindLower === \"category\") {\n const count =\n typeof item.categoryCount === \"number\" ? item.categoryCount : null;\n return count !== null\n ? `Managing ${count} ${count === 1 ? \"category\" : \"categories\"}`\n : \"Category manager\";\n }\n if (kindLower === \"torrent\") {\n const metricType = item.metricType?.toLowerCase();\n const categoryTotal =\n typeof item.categoryCount === \"number\" ? item.categoryCount : null;\n const queueTotal =\n typeof item.queueCount === \"number\" ? item.queueCount : null;\n\n if (!metricType) {\n const queueLabel = queueTotal !== null ? queueTotal : \"?\";\n const categoryLabel = categoryTotal !== null ? categoryTotal : \"?\";\n return `Torrents in queue ${queueLabel} / total ${categoryLabel}`;\n }\n\n if (metricType === \"category\" && categoryTotal !== null) {\n return `Torrent count ${categoryTotal}`;\n }\n\n if (metricType === \"free-space\" && queueTotal !== null) {\n return `Torrent count ${queueTotal}`;\n }\n\n return \"Torrent count unavailable\";\n }\n return \"\";\n })()}\n </div>\n <div className=\"process-chip__actions\">\n <button\n className=\"btn small\"\n onClick={() => handleRestart(item.category, item.kind)}\n >\n Restart\n </button>\n </div>\n </div>\n ))}\n {instanceCategories.map((cat) => {\n const instanceAlive = statusData?.qbitInstances?.[cat.instance]?.alive ?? false;\n const detail = cat.seedingCount > 0\n ? `${cat.torrentCount} torrents (${cat.seedingCount} seeding)`\n : `${cat.torrentCount} torrents`;\n return (\n <div className=\"process-chip process-chip--info\" key={`cat:${cat.instance}:${cat.category}`}>\n <div className=\"process-chip__top\">\n <div className=\"process-chip__name\">\n {cat.category}\n <span className=\"process-chip__managed-badge\">\n {cat.managedBy === \"arr\" ? \"Arr\" : \"qBit\"}\n </span>\n </div>\n <div className={`status-pill__dot ${instanceAlive ? \"text-success\" : \"text-danger\"}`} />\n </div>\n <div className=\"process-chip__detail\">{detail}</div>\n </div>\n );\n })}\n </div>\n <div className=\"process-card__footer\">\n <button\n className=\"btn small\"\n onClick={() => void handleRestartGroup(items)}\n >\n Restart All\n </button>\n </div>\n </div>\n );\n });\n return { app, cards };\n });\n\n return (\n <>\n <section className=\"card\">\n <div className=\"card-header\">Processes</div>\n <div className=\"card-body stack\">\n <div className=\"row\">\n <div className=\"col inline\">\n <button className=\"btn ghost\" onClick={() => void load()} disabled={loading}>\n {loading && <span className=\"spinner\" />}\n <IconImage src={RefreshIcon} />\n {loading ? 'Refreshing...' : 'Refresh'}\n </button>\n <button className=\"btn\" onClick={() => void handleRestartAll()} disabled={restartingAll}>\n {restartingAll && <span className=\"spinner\" />}\n <IconImage src={RestartIcon} />\n {restartingAll ? 'Restarting...' : 'Restart All'}\n </button>\n <button className=\"btn\" onClick={() => void handleRebuildArrs()} disabled={rebuildingArrs}>\n {rebuildingArrs && <span className=\"spinner\" />}\n <IconImage src={ToolsIcon} />\n {rebuildingArrs ? 'Rebuilding...' : 'Rebuild Arrs'}\n </button>\n </div>\n </div>\n {cardsByApp.length ? (\n cardsByApp.map(({ app, cards }) => (\n <div className=\"process-section\" key={app}>\n <div className=\"process-section__title\">{app}</div>\n <div className=\"process-grid\">{cards}</div>\n </div>\n ))\n ) : (\n <div className=\"empty-state\">No processes available.</div>\n )}\n </div>\n </section>\n {confirmAction && (\n <ConfirmDialog\n title={confirmAction.title}\n message={confirmAction.message}\n confirmLabel=\"Confirm\"\n cancelLabel=\"Cancel\"\n danger={true}\n onConfirm={confirmAction.onConfirm}\n onCancel={() => setConfirmAction(null)}\n />\n )}\n </>\n );\n}\n"],"names":["ConfirmDialog","title","message","confirmLabel","cancelLabel","onConfirm","onCancel","danger","jsx","jsxs","e","IconImage","CloseIcon","ToolsIcon","RELEASE_TOKEN_REGEX","EPISODE_TOKEN_REGEX","SEASON_TOKEN_REGEX","sanitizeSearchSummary","raw","trimmed","normalized","releaseMatch","rest","looksLikeEpisode","cleanedTitle","year","isProcessEqual","a","b","areProcessListsEqual","index","getRefreshDelay","active","ProcessesView","processes","setProcesses","useState","loading","setLoading","restartingAll","setRestartingAll","rebuildingArrs","setRebuildingArrs","statusData","setStatusData","qbitCategories","setQbitCategories","confirmAction","setConfirmAction","push","useToast","isFetching","useRef","load","useCallback","showLoading","processData","status","categoriesData","getProcesses","getStatus","getQbitCategories","next","process","sanitized","prev","error","useEffect","refreshDelay","useMemo","useInterval","handleRestart","category","kind","restartProcess","handleRestartAll","restartAllProcesses","handleRebuildArrs","rebuildArrs","groupedProcesses","appBuckets","classifyApp","proc","name","arrs","hasRadarr","arr","hasSonarr","hasLidarr","app","instances","instanceKey","appOrder","result","sortedInstances","items","group","order","label","handleRestartGroup","item","cardsByApp","cards","instanceName","instanceCategories","cat","nameLower","instLower","runningCount","totalCount","tone","statusClass","statusLabel","summaryLabel","displayName","filteredKinds","lower","formatKind","kindLower","count","metricType","categoryTotal","queueTotal","instanceAlive","detail","Fragment","RefreshIcon","RestartIcon"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{u as B,k as R,b,j as s,I as p,R as z}from"./app.js";import{r as g}from"./table.js";import{u as M}from"./useInterval.js";import{S as N}from"./StableTable.js";import"./vendor.js";function y(n){if(n===0)return"0 B";const t=1024,i=["B","KB","MB","GB","TB"],a=Math.floor(Math.log(n)/Math.log(t));return`${(n/t**a).toFixed(2)} ${i[a]}`}function C(n){const t=Math.round(n);if(t===0)return"0s";const i=Math.floor(t/86400),a=Math.floor(t%86400/3600),r=Math.floor(t%3600/60),u=t%60,l=[];return i>0&&l.push(`${i}d`),a>0&&l.push(`${a}h`),r>0&&l.push(`${r}m`),(u>0||l.length===0)&&l.push(`${u}s`),l.join(" ")}function V(n){switch(n){case-1:return"Never";case 1:return"On Ratio";case 2:return"On Time";case 3:return"Ratio OR Time";case 4:return"Ratio AND Time";default:return"Unknown"}}function q(n,t){if(n===t)return!0;if(n.length!==t.length)return!1;for(let i=0;i<n.length;i+=1){const a=n[i],r=t[i];if(a.category!==r.category||a.instance!==r.instance||a.torrentCount!==r.torrentCount||a.seedingCount!==r.seedingCount||a.totalSize!==r.totalSize||a.avgRatio!==r.avgRatio||a.avgSeedingTime!==r.avgSeedingTime||a.managedBy!==r.managedBy)return!1}return!0}function w({active:n}){const[t,i]=g.useState([]),[a,r]=g.useState(!1),{push:u}=B(),{liveArr:l}=R(),x=g.useRef(!1),m=g.useCallback(async(e=!0)=>{if(!x.current){x.current=!0,e&&r(!0);try{const o=await b();i(f=>q(f,o.categories)?f:o.categories)}catch(o){u(o instanceof Error?o.message:"Failed to load qBit categories","error")}finally{x.current=!1,e&&r(!1)}}},[u]);g.useEffect(()=>{n&&m()},[n,m]),M(()=>{m(!1)},l?1e3:null);const v=g.useCallback(()=>{m()},[m]),d=g.useMemo(()=>{const e=t.reduce((c,h)=>c+h.torrentCount,0),o=t.reduce((c,h)=>c+h.seedingCount,0),f=t.reduce((c,h)=>c+h.totalSize,0),j=t.filter(c=>c.managedBy==="qbit").length,T=t.filter(c=>c.managedBy==="arr").length;return{totalTorrents:e,totalSeeding:o,totalSize:f,qbitCount:j,arrCount:T,categoryCount:t.length}},[t]),S=g.useMemo(()=>[{accessorKey:"category",header:"Category",cell:e=>e.getValue(),size:150},{accessorKey:"managedBy",header:"Managed By",cell:e=>e.getValue()==="qbit"?s.jsx("span",{className:"badge badge-qbit",children:"qBit"}):s.jsx("span",{className:"badge badge-arr",children:"Arr"}),size:120},{accessorKey:"instance",header:"Instance",cell:e=>e.getValue(),size:150},{accessorKey:"torrentCount",header:"Torrents",cell:e=>e.getValue().toLocaleString(),size:100},{accessorKey:"seedingCount",header:"Seeding",cell:e=>e.getValue().toLocaleString(),size:100},{accessorKey:"totalSize",header:"Total Size",cell:e=>y(e.getValue()),size:120},{accessorKey:"avgRatio",header:"Avg Ratio",cell:e=>e.getValue().toFixed(2),size:100},{accessorKey:"avgSeedingTime",header:"Avg Seed Time",cell:e=>C(Math.round(e.getValue())),size:140},{id:"maxRatio",header:"Max Ratio",accessorFn:e=>e.seedingConfig.maxRatio,cell:e=>{const o=e.getValue();return o===-1?"Disabled":o.toFixed(2)},size:100},{id:"maxTime",header:"Max Time",accessorFn:e=>e.seedingConfig.maxTime,cell:e=>{const o=e.getValue();return o===-1?"Disabled":C(o)},size:120},{id:"removeMode",header:"Remove Mode",accessorFn:e=>e.seedingConfig.removeMode,cell:e=>V(e.getValue()),size:150}],[]);return s.jsxs("div",{className:"stack animate-fade-in",children:[s.jsxs("div",{className:"row",style:{justifyContent:"space-between"},children:[s.jsxs("div",{className:"hint",children:["Category overview across all instances",s.jsx("br",{}),s.jsx("strong",{children:"Categories:"})," ",d.categoryCount," •"," ",s.jsx("strong",{children:"qBit-managed:"})," ",d.qbitCount," •"," ",s.jsx("strong",{children:"Arr-managed:"})," ",d.arrCount," •"," ",s.jsx("strong",{children:"Total Torrents:"})," ",d.totalTorrents.toLocaleString()," •"," ",s.jsx("strong",{children:"Seeding:"})," ",d.totalSeeding.toLocaleString()," •"," ",s.jsx("strong",{children:"Total Size:"})," ",y(d.totalSize)]}),s.jsxs("button",{className:"btn ghost",onClick:v,disabled:a,children:[a&&s.jsx("span",{className:"spinner"}),s.jsx(p,{src:z}),a?"Refreshing...":"Refresh"]})]}),a&&t.length===0?s.jsxs("div",{className:"loading",children:[s.jsx("span",{className:"spinner"})," Loading categories…"]}):t.length===0?s.jsx("div",{className:"hint",children:"No categories found. Configure ManagedCategories in your qBit config sections or add Arr instances to see categories."}):s.jsx(N,{data:t,columns:S,getRowKey:e=>`${e.instance}-${e.category}-${e.managedBy}`})]})}export{w as QbitCategoriesView};
|
|
2
|
+
//# sourceMappingURL=QbitCategoriesView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QbitCategoriesView.js","sources":["../../../webui/src/pages/QbitCategoriesView.tsx"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport { getQbitCategories } from \"../api/client\";\nimport type { QbitCategory } from \"../api/types\";\nimport { useToast } from \"../context/ToastContext\";\nimport { useInterval } from \"../hooks/useInterval\";\nimport { useWebUI } from \"../context/WebUIContext\";\nimport { StableTable } from \"../components/StableTable\";\nimport {\n useReactTable,\n getCoreRowModel,\n getSortedRowModel,\n flexRender,\n type ColumnDef,\n} from \"@tanstack/react-table\";\nimport { IconImage } from \"../components/IconImage\";\nimport RefreshIcon from \"../icons/refresh-arrow.svg\";\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 B\";\n const k = 1024;\n const sizes = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return `${(bytes / k ** i).toFixed(2)} ${sizes[i]}`;\n}\n\nfunction formatTime(seconds: number): string {\n const totalSeconds = Math.round(seconds);\n if (totalSeconds === 0) return \"0s\";\n\n const days = Math.floor(totalSeconds / 86400);\n const hours = Math.floor((totalSeconds % 86400) / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n\n const parts: string[] = [];\n if (days > 0) parts.push(`${days}d`);\n if (hours > 0) parts.push(`${hours}h`);\n if (minutes > 0) parts.push(`${minutes}m`);\n if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);\n\n return parts.join(\" \");\n}\n\nfunction getRemoveModeText(mode: number): string {\n switch (mode) {\n case -1:\n return \"Never\";\n case 1:\n return \"On Ratio\";\n case 2:\n return \"On Time\";\n case 3:\n return \"Ratio OR Time\";\n case 4:\n return \"Ratio AND Time\";\n default:\n return \"Unknown\";\n }\n}\n\nfunction areCategoriesEqual(a: QbitCategory[], b: QbitCategory[]): boolean {\n if (a === b) return true;\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i += 1) {\n const catA = a[i];\n const catB = b[i];\n if (\n catA.category !== catB.category ||\n catA.instance !== catB.instance ||\n catA.torrentCount !== catB.torrentCount ||\n catA.seedingCount !== catB.seedingCount ||\n catA.totalSize !== catB.totalSize ||\n catA.avgRatio !== catB.avgRatio ||\n catA.avgSeedingTime !== catB.avgSeedingTime ||\n catA.managedBy !== catB.managedBy\n ) {\n return false;\n }\n }\n return true;\n}\n\ninterface QbitCategoriesViewProps {\n active: boolean;\n}\n\nexport function QbitCategoriesView({ active }: QbitCategoriesViewProps): JSX.Element {\n const [categories, setCategories] = useState<QbitCategory[]>([]);\n const [loading, setLoading] = useState(false);\n const { push } = useToast();\n const { liveArr } = useWebUI();\n const isFetching = useRef(false);\n\n const load = useCallback(\n async (showLoading = true) => {\n if (isFetching.current) {\n return;\n }\n isFetching.current = true;\n if (showLoading) {\n setLoading(true);\n }\n try {\n const data = await getQbitCategories();\n setCategories((prev) =>\n areCategoriesEqual(prev, data.categories) ? prev : data.categories\n );\n } catch (error) {\n push(\n error instanceof Error\n ? error.message\n : \"Failed to load qBit categories\",\n \"error\"\n );\n } finally {\n isFetching.current = false;\n if (showLoading) {\n setLoading(false);\n }\n }\n },\n [push]\n );\n\n useEffect(() => {\n if (active) {\n void load();\n }\n }, [active, load]);\n\n useInterval(\n () => {\n void load(false);\n },\n liveArr ? 1000 : null\n );\n\n const handleRefresh = useCallback(() => {\n void load();\n }, [load]);\n\n // Calculate summary stats\n const summary = useMemo(() => {\n const totalTorrents = categories.reduce((sum, cat) => sum + cat.torrentCount, 0);\n const totalSeeding = categories.reduce((sum, cat) => sum + cat.seedingCount, 0);\n const totalSize = categories.reduce((sum, cat) => sum + cat.totalSize, 0);\n const qbitCount = categories.filter((cat) => cat.managedBy === \"qbit\").length;\n const arrCount = categories.filter((cat) => cat.managedBy === \"arr\").length;\n\n return {\n totalTorrents,\n totalSeeding,\n totalSize,\n qbitCount,\n arrCount,\n categoryCount: categories.length,\n };\n }, [categories]);\n\n // Define table columns\n const columns = useMemo<ColumnDef<QbitCategory>[]>(\n () => [\n {\n accessorKey: \"category\",\n header: \"Category\",\n cell: (info) => info.getValue(),\n size: 150,\n },\n {\n accessorKey: \"managedBy\",\n header: \"Managed By\",\n cell: (info) => {\n const managedBy = info.getValue() as \"qbit\" | \"arr\";\n return managedBy === \"qbit\" ? (\n <span className=\"badge badge-qbit\">qBit</span>\n ) : (\n <span className=\"badge badge-arr\">Arr</span>\n );\n },\n size: 120,\n },\n {\n accessorKey: \"instance\",\n header: \"Instance\",\n cell: (info) => info.getValue(),\n size: 150,\n },\n {\n accessorKey: \"torrentCount\",\n header: \"Torrents\",\n cell: (info) => (info.getValue() as number).toLocaleString(),\n size: 100,\n },\n {\n accessorKey: \"seedingCount\",\n header: \"Seeding\",\n cell: (info) => (info.getValue() as number).toLocaleString(),\n size: 100,\n },\n {\n accessorKey: \"totalSize\",\n header: \"Total Size\",\n cell: (info) => formatBytes(info.getValue() as number),\n size: 120,\n },\n {\n accessorKey: \"avgRatio\",\n header: \"Avg Ratio\",\n cell: (info) => (info.getValue() as number).toFixed(2),\n size: 100,\n },\n {\n accessorKey: \"avgSeedingTime\",\n header: \"Avg Seed Time\",\n cell: (info) => formatTime(Math.round(info.getValue() as number)),\n size: 140,\n },\n {\n id: \"maxRatio\",\n header: \"Max Ratio\",\n accessorFn: (row) => row.seedingConfig.maxRatio,\n cell: (info) => {\n const val = info.getValue() as number;\n return val === -1 ? \"Disabled\" : val.toFixed(2);\n },\n size: 100,\n },\n {\n id: \"maxTime\",\n header: \"Max Time\",\n accessorFn: (row) => row.seedingConfig.maxTime,\n cell: (info) => {\n const val = info.getValue() as number;\n return val === -1 ? \"Disabled\" : formatTime(val);\n },\n size: 120,\n },\n {\n id: \"removeMode\",\n header: \"Remove Mode\",\n accessorFn: (row) => row.seedingConfig.removeMode,\n cell: (info) => getRemoveModeText(info.getValue() as number),\n size: 150,\n },\n ],\n []\n );\n\n return (\n <div className=\"stack animate-fade-in\">\n <div className=\"row\" style={{ justifyContent: \"space-between\" }}>\n <div className=\"hint\">\n Category overview across all instances\n <br />\n <strong>Categories:</strong> {summary.categoryCount} •{\" \"}\n <strong>qBit-managed:</strong> {summary.qbitCount} •{\" \"}\n <strong>Arr-managed:</strong> {summary.arrCount} •{\" \"}\n <strong>Total Torrents:</strong>{\" \"}\n {summary.totalTorrents.toLocaleString()} •{\" \"}\n <strong>Seeding:</strong> {summary.totalSeeding.toLocaleString()} •{\" \"}\n <strong>Total Size:</strong> {formatBytes(summary.totalSize)}\n </div>\n <button className=\"btn ghost\" onClick={handleRefresh} disabled={loading}>\n {loading && <span className=\"spinner\" />}\n <IconImage src={RefreshIcon} />\n {loading ? \"Refreshing...\" : \"Refresh\"}\n </button>\n </div>\n\n {loading && categories.length === 0 ? (\n <div className=\"loading\">\n <span className=\"spinner\" /> Loading categories…\n </div>\n ) : categories.length === 0 ? (\n <div className=\"hint\">\n No categories found. Configure ManagedCategories in your qBit config sections or\n add Arr instances to see categories.\n </div>\n ) : (\n <StableTable\n data={categories}\n columns={columns}\n getRowKey={(cat) => `${cat.instance}-${cat.category}-${cat.managedBy}`}\n />\n )}\n </div>\n );\n}\n"],"names":["formatBytes","bytes","k","sizes","i","formatTime","seconds","totalSeconds","days","hours","minutes","secs","parts","getRemoveModeText","mode","areCategoriesEqual","a","b","catA","catB","QbitCategoriesView","active","categories","setCategories","useState","loading","setLoading","push","useToast","liveArr","useWebUI","isFetching","useRef","load","useCallback","showLoading","data","getQbitCategories","prev","error","useEffect","useInterval","handleRefresh","summary","useMemo","totalTorrents","sum","cat","totalSeeding","totalSize","qbitCount","arrCount","columns","info","jsx","row","val","jsxs","IconImage","RefreshIcon","StableTable"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{j as e}from"./app.js";import{r as i,u as c,f as d,a as m}from"./table.js";function u({data:l,columns:s,getRowKey:r}){const o=c({data:l,columns:s,getCoreRowModel:m()});return e.jsx("div",{className:"table-wrapper",children:e.jsxs("table",{className:"responsive-table",children:[e.jsx("thead",{children:o.getHeaderGroups().map(t=>e.jsx("tr",{children:t.headers.map(a=>e.jsx("th",{children:a.isPlaceholder?null:d(a.column.columnDef.header,a.getContext())},a.id))},t.id))}),e.jsx("tbody",{children:o.getRowModel().rows.map(t=>{const a=r?r(t.original):t.id;return e.jsx("tr",{children:t.getVisibleCells().map(n=>e.jsx("td",{"data-label":String(n.column.columnDef.header),children:d(n.column.columnDef.cell,n.getContext())},n.id))},a)})})]})})}const h=i.memo(u,(l,s)=>l.data===s.data&&l.columns===s.columns);export{h as S};
|
|
2
|
+
//# sourceMappingURL=StableTable.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StableTable.js","sources":["../../../webui/src/components/StableTable.tsx"],"sourcesContent":["import { memo } from \"react\";\nimport {\n useReactTable,\n getCoreRowModel,\n flexRender,\n type ColumnDef,\n} from \"@tanstack/react-table\";\n\ninterface StableTableProps<TData> {\n data: TData[];\n columns: ColumnDef<TData, unknown>[];\n getRowKey?: (row: TData) => string;\n}\n\nfunction StableTableInner<TData>({ data, columns, getRowKey }: StableTableProps<TData>) {\n const table = useReactTable({\n data,\n columns,\n getCoreRowModel: getCoreRowModel(),\n });\n\n return (\n <div className=\"table-wrapper\">\n <table className=\"responsive-table\">\n <thead>\n {table.getHeaderGroups().map((headerGroup) => (\n <tr key={headerGroup.id}>\n {headerGroup.headers.map((header) => (\n <th key={header.id}>\n {header.isPlaceholder\n ? null\n : flexRender(\n header.column.columnDef.header,\n header.getContext()\n )}\n </th>\n ))}\n </tr>\n ))}\n </thead>\n <tbody>\n {table.getRowModel().rows.map((row) => {\n const stableKey = getRowKey ? getRowKey(row.original) : row.id;\n return (\n <tr key={stableKey}>\n {row.getVisibleCells().map((cell) => (\n <td key={cell.id} data-label={String(cell.column.columnDef.header)}>\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\n </td>\n ))}\n </tr>\n );\n })}\n </tbody>\n </table>\n </div>\n );\n}\n\nexport const StableTable = memo(StableTableInner, (prevProps, nextProps) => {\n // Only re-render if data reference changed\n return prevProps.data === nextProps.data && prevProps.columns === nextProps.columns;\n}) as typeof StableTableInner;\n"],"names":["StableTableInner","data","columns","getRowKey","table","useReactTable","getCoreRowModel","jsxs","jsx","headerGroup","header","flexRender","row","stableKey","cell","StableTable","memo","prevProps","nextProps"],"mappings":"iFAcA,SAASA,EAAwB,CAAE,KAAAC,EAAM,QAAAC,EAAS,UAAAC,GAAsC,CACtF,MAAMC,EAAQC,EAAc,CAC1B,KAAAJ,EACA,QAAAC,EACA,gBAAiBI,EAAA,CAAgB,CAClC,EAED,aACG,MAAA,CAAI,UAAU,gBACb,SAAAC,EAAAA,KAAC,QAAA,CAAM,UAAU,mBACf,SAAA,CAAAC,EAAAA,IAAC,SACE,SAAAJ,EAAM,kBAAkB,IAAKK,GAC5BD,EAAAA,IAAC,KAAA,CACE,SAAAC,EAAY,QAAQ,IAAKC,SACvB,KAAA,CACE,SAAAA,EAAO,cACJ,KACAC,EACED,EAAO,OAAO,UAAU,OACxBA,EAAO,WAAA,CAAW,CACpB,EANGA,EAAO,EAOhB,CACD,GAVMD,EAAY,EAWrB,CACD,EACH,EACAD,MAAC,SACE,SAAAJ,EAAM,YAAA,EAAc,KAAK,IAAKQ,GAAQ,CACrC,MAAMC,EAAYV,EAAYA,EAAUS,EAAI,QAAQ,EAAIA,EAAI,GAC5D,OACEJ,EAAAA,IAAC,KAAA,CACE,SAAAI,EAAI,gBAAA,EAAkB,IAAKE,GAC1BN,EAAAA,IAAC,KAAA,CAAiB,aAAY,OAAOM,EAAK,OAAO,UAAU,MAAM,EAC9D,SAAAH,EAAWG,EAAK,OAAO,UAAU,KAAMA,EAAK,WAAA,CAAY,CAAA,EADlDA,EAAK,EAEd,CACD,GALMD,CAMT,CAEJ,CAAC,CAAA,CACH,CAAA,CAAA,CACF,CAAA,CACF,CAEJ,CAEO,MAAME,EAAcC,EAAAA,KAAKhB,EAAkB,CAACiB,EAAWC,IAErDD,EAAU,OAASC,EAAU,MAAQD,EAAU,UAAYC,EAAU,OAC7E"}
|