diff --git a/CLAUDE.md b/CLAUDE.md index 3f2ebad..aa9c4eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -470,7 +470,8 @@ still hear the news. Four publishers today: repeating an outage every minute would bury everything else. This is the alert that earns the banner its place over playback: video direct-plays from Emby, so when Emby stops answering the film stalls with no explanation, and the gateway is still up to - say why. `MEMBY_EMBY_HEALTH_INTERVAL=0` turns the probe and both banners off. + say why when the household permits playback overlays. `MEMBY_EMBY_HEALTH_INTERVAL=0` + turns the probe and both banners off. `mergeAlerts` interleaves derived and published alerts newest-first and caps at `maxAlerts`, which is why every alert carries a timestamp. Alerts also carry their own @@ -492,13 +493,22 @@ because stealing D-pad focus mid-browse is worse than a missed notice; and alerts are suppressed under maintenance and under a mandatory update, which own the screen. -**The bar appears over playback too**, not only over the launcher: `PlayerActivity` -mounts the same composable in a `ComposeView` (`player_service_alerts` in -`activity_player.xml`, declared before the loading and error overlays so those cover it). +**Where the bar may appear is the server's decision.** `GatewaySettings.NotificationDisplay` +is `everywhere`, `home_only` (the default) or `off`, and `/v1/status` carries the effective +value on every poll. `NotificationDisplayPolicy.fromWire` defaults missing and unknown +values to Home only, and `notificationEligible` is the single rule every renderer uses: +Home only admits browsing and refuses playback, Everywhere admits both, and Off admits +neither. The server also omits `alerts` under Off, but the client still applies the rule so +an alert already offered cannot remain visible after an operator changes the setting. +Only the call sites identify their `NotificationSurface`; no screen owns policy logic. + +When Everywhere is selected, `PlayerActivity` mounts the same composable in a `ComposeView` +(`player_service_alerts` in `activity_player.xml`, declared before the loading and error +overlays so those cover it). `alertsSuppressed` starts *true* and is cleared only by `hidePlaybackLoading`, which is reached once the preroll is over and the first frame is up — an alert composed behind an overlay would be marked seen by a viewer who never saw it, which is the same failure -`alertShown` exists to prevent. Because it now covers somebody's film, the bar is +`alertShown` exists to prevent. Because it can cover somebody's film, the bar is deliberately small (76dp), near-black, and eases in over ~680ms rather than snapping down. It shows the **Emby mark**, not item artwork: a library refresh and an outage have no artwork, and one constant mark reads as "your server is talking" where a poster made diff --git a/admin-ui/dist/assets/index-BQVB9Fvj.js b/admin-ui/dist/assets/index-BQVB9Fvj.js deleted file mode 100644 index 5aaeff7..0000000 --- a/admin-ui/dist/assets/index-BQVB9Fvj.js +++ /dev/null @@ -1,11 +0,0 @@ -import{r as g,a as On,u as ws,L as Z,b as ks,m as vs,N as xn,O as Pn,c as Be,B as _n,R as Un,d as W,e as As}from"./router-D9WH5XEU.js";(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))a(i);new MutationObserver(i=>{for(const l of i)if(l.type==="childList")for(const r of l.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&a(r)}).observe(document,{childList:!0,subtree:!0});function t(i){const l={};return i.integrity&&(l.integrity=i.integrity),i.referrerPolicy&&(l.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?l.credentials="include":i.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function a(i){if(i.ep)return;i.ep=!0;const l=t(i);fetch(i.href,l)}})();var jn={exports:{}},cs={};/** - * @license React - * react-jsx-runtime.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Wn=g,Bn=Symbol.for("react.element"),Vn=Symbol.for("react.fragment"),Hn=Object.prototype.hasOwnProperty,zn=Wn.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Kn={key:!0,ref:!0,__self:!0,__source:!0};function gn(s,n,t){var a,i={},l=null,r=null;t!==void 0&&(l=""+t),n.key!==void 0&&(l=""+n.key),n.ref!==void 0&&(r=n.ref);for(a in n)Hn.call(n,a)&&!Kn.hasOwnProperty(a)&&(i[a]=n[a]);if(s&&s.defaultProps)for(a in n=s.defaultProps,n)i[a]===void 0&&(i[a]=n[a]);return{$$typeof:Bn,type:s,key:l,ref:r,props:i,_owner:zn.current}}cs.Fragment=Vn;cs.jsx=gn;cs.jsxs=gn;jn.exports=cs;var e=jn.exports,vn,Es=On;vn=Es.createRoot,Es.hydrateRoot;const bn={overview:"M4 13h6V4H4zm0 7h6v-4H4zm10 0h6v-9h-6zm0-16v4h6V4z",library:"M3 5h18v14H3zM7 5v14M17 5v14M3 9.5h4M3 14.5h4M17 9.5h4M17 14.5h4",people:"M15 19v-1.2a3.3 3.3 0 0 0-3.3-3.3H6.8A3.3 3.3 0 0 0 3.5 17.8V19M9.2 11a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4ZM17 10.6a3 3 0 0 0-1.4-5.7M20.5 19v-1.2a3.3 3.3 0 0 0-2.4-3.2",person:"M18 20v-1.5a4 4 0 0 0-4-4h-4a4 4 0 0 0-4 4V20M12 10.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z",tv:"M4 5h16v10H4zM9 19h6M12 15v4M8 2.5 12 5l4-2.5",sliders:"M4 7h10M18 7h2M4 17h2m4 0h10M14 4v6M6 14v6",clock:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 7.2V12l3 1.8",pulse:"M3 12h3.5L9 19l5-14 2.5 7H21",chart:"M4 19V9m5 10V5m5 14v-7m5 7V3",chip:"M8 8h8v8H8zM4.5 4.5h15v15h-15zM9 2v2.5M15 2v2.5M9 19.5V22M15 19.5V22M2 9h2.5M2 15h2.5M19.5 9H22M19.5 15H22",database:"M12 8.2c4.4 0 8-1.2 8-2.6S16.4 3 12 3 4 4.2 4 5.6s3.6 2.6 8 2.6ZM4 5.6v12.8C4 19.8 7.6 21 12 21s8-1.2 8-2.6V5.6M4 12c0 1.4 3.6 2.6 8 2.6s8-1.2 8-2.6",download:"M12 3.5v10m0 0 4-4m-4 4-4-4M4.5 18h15",upload:"M12 16V4m0 0L8 8m4-4 4 4M5 13v6h14v-6",sync:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",search:"M10.5 17a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Zm4.6-1.9L20 20",star:"m12 3.2 2.6 5.4 5.9.8-4.3 4.1 1 5.9-5.2-2.8-5.2 2.8 1-5.9L3.5 9.4l5.9-.8L12 3.2Z",sparkle:"m10 3 1.5 4.3L16 8.8l-4.5 1.5L10 14.6 8.5 10.3 4 8.8l4.5-1.5L10 3ZM17.5 14l.9 2.4 2.6.9-2.6.9-.9 2.4-.9-2.4-2.6-.9 2.6-.9.9-2.4Z",bell:"M6.2 9.5a5.8 5.8 0 1 1 11.6 0c0 4.6 2.2 5.9 2.2 5.9H4s2.2-1.3 2.2-5.9M10 19.5a2 2 0 0 0 4 0",shield:"m12 3 7.5 3v5.4c0 5-3.2 8.2-7.5 9.6-4.3-1.4-7.5-4.6-7.5-9.6V6L12 3Zm-2.6 8.7 1.9 1.9 3.6-3.6",wrench:"m14.5 6.5 3-3 3 3-3 3M9 15l-5.5 5.5M13 4a5 5 0 0 0 6.5 6.5L10 20l-6-6 9.5-9.5Z",play:"M8 5.2v13.6L19 12 8 5.2ZM4 5v14",list:"M4 7h16M4 12h16M4 17h10",inbox:"M4 7h16v13H4zM8 4h8v3M8 12h8M8 16h5",send:"M21 3 10.5 13.5M21 3l-6.8 18-3.7-7.5L3 10z",history:"M3.5 12a8.5 8.5 0 1 0 2.8-6.3M3.5 4v4h4M12 7.5V12l3 1.8",check:"m5 12.5 4.5 4.5L19 7.5",alert:"M12 8.5v5m0 3.2h.01M10.3 4.4 2.7 17.5a2 2 0 0 0 1.7 3h15.2a2 2 0 0 0 1.7-3L13.7 4.4a2 2 0 0 0-3.4 0Z",power:"M12 3v9M7.5 6.2a7.5 7.5 0 1 0 9 0",key:"M14.5 3a6.5 6.5 0 1 0 3.4 12L19 14h2v-2h2V9.5l-2.5-2.5A6.5 6.5 0 0 0 14.5 3Zm-2.6 4.6a1.6 1.6 0 1 1-2.3 2.3 1.6 1.6 0 0 1 2.3-2.3Z",captions:"M4 5.5h16v13H4zM7 15h5m3 0h2M7 11h3m2 0h5",journey:"M4 6h5v5h6v7h5M7 3 4 6l3 3m10 6 3 3-3 3",plug:"M9 3v6M15 3v6M6.5 9h11v3.5a5.5 5.5 0 0 1-11 0zM12 18v3",calendar:"M4 6h16v15H4zM8 3v5M16 3v5M4 11h16",trash:"M4 7h16M9 7V4.5h6V7M6.5 7l1 13h9l1-13M10 11v5M14 11v5",plus:"M12 5v14M5 12h14",close:"M6 6l12 12M18 6 6 18",caret:"m6 9 6 6 6-6",external:"M14 4h6v6M20 4l-9 9M18 14v5.5H4.5V6H10",refresh:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",filter:"M3.5 5.5h17l-6.5 7.5V20l-4-2v-5L3.5 5.5Z",menu:"M4 7h16M4 12h16M4 17h16",globe:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3.5 9h17M3.5 15h17M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18Z",logout:"M15 17l5-5-5-5M20 12H9M12 4H5v16h7"};function se({name:s,className:n}){const t=bn[s];return t?e.jsx("svg",{className:n??"ico",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:e.jsx("path",{d:t})}):null}function Qe({name:s,tone:n}){return bn[s]?e.jsx("span",{className:"glyph","data-tone":n,children:e.jsx(se,{name:s})}):null}const Oe=[{id:"everyday",label:"Everyday",defaultCollapsed:!1,items:[{id:"overview",path:"/admin",label:"Overview",title:"Overview",intro:"What the gateway is doing right now.",icon:"overview"},{id:"activity",path:"/admin/activity",label:"Activity",title:"Activity",intro:"Every administrative event, newest first.",icon:"bell",badge:"notifications"},{id:"accounts",path:"/admin/accounts",label:"Users",title:"Users",intro:"Who uses Memby, and the devices they are signed in on.",icon:"people"},{id:"requests",path:"/admin/requests",label:"Media requests",title:"Media requests",intro:"Who can ask for something the library does not have.",icon:"inbox"},{id:"notifications",path:"/admin/notifications",label:"Notifications",title:"Notifications",intro:"Everything Memby sent: who it went to, over which channel, and whether it worked.",icon:"send"},{id:"media-reports",path:"/admin/media-reports",label:"Media reports",title:"Media reports",intro:"Problems viewers reported with a film or episode.",icon:"alert"},{id:"updates",path:"/admin/updates",label:"App updates",title:"App updates",intro:"Publish an optional or a required client update.",icon:"upload"},{id:"logs",path:"/admin/logs",label:"Logs",title:"Logs",intro:"Structured gateway events as they happen.",icon:"list"}]},{id:"people",label:"Devices & access",defaultCollapsed:!0,items:[{id:"account",path:"/admin/accounts/:userId",label:"User",title:"User",intro:"Devices, recommendation setup and synced settings for one person.",icon:"person",hidden:!0},{id:"settings-history",path:"/admin/accounts/:userId/settings",label:"Settings history",title:"Settings history",intro:"Every change to one person's synced settings, and which devices took it.",icon:"sliders",hidden:!0},{id:"clients",path:"/admin/clients",label:"Devices",title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands.",icon:"tv"},{id:"logins",path:"/admin/logins",label:"Sign-ins",title:"Sign-in history",intro:"Every connection attempt: who, which television, from where, and whether it got in.",icon:"key"},{id:"device",path:"/admin/devices/:deviceId",label:"Device",title:"Device",intro:"One television: how often it connects, at what times, and from which addresses.",icon:"tv",hidden:!0}]},{id:"content",label:"Content & discovery",defaultCollapsed:!0,items:[{id:"library",path:"/admin/library",label:"Library",title:"Library",intro:"Import and inspect the catalogue Memby ranks.",icon:"library"},{id:"hero",path:"/admin/hero",label:"Home hero",title:"Home hero",intro:"Choose films or television shows for the launcher spotlight while recent releases fill the remaining places.",icon:"star"},{id:"metadata-hero",path:"/admin/metadata-hero",label:"Metadata hero",title:"Metadata hero",intro:"Order the focused-title information shown above browse rows on every television.",icon:"tv"},{id:"recommendations",path:"/admin/recommendations",label:"For You",title:"For You",intro:"The prepared pools personalised rows are drawn from.",icon:"sparkle"},{id:"inspector",path:"/admin/inspector",label:"Score inspector",title:"Score inspector",intro:"Re-run the ranker for one person and read every component.",icon:"search"}]},{id:"experience",label:"Viewing experience",defaultCollapsed:!0,items:[{id:"features",path:"/admin/features",label:"Features",title:"Features",intro:"Roll out, stop and recover optional behaviour with no app release.",icon:"sliders"},{id:"playback",path:"/admin/playback",label:"Playback",title:"Playback",intro:"Presentation policy sent with every playback launch.",icon:"play"},{id:"subtitles",path:"/admin/subtitles",label:"Subtitles",title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from.",icon:"captions"},{id:"credits",path:"/admin/credits",label:"Credits detection",title:"Credits detection",intro:"Control predictive scanning and review every completed credits scan.",icon:"clock"}]},{id:"operations",label:"Operations",defaultCollapsed:!0,items:[{id:"tasks",path:"/admin/tasks",label:"Scheduled tasks",title:"Scheduled tasks",intro:"What the gateway does in the background, when it last ran and whether it worked.",icon:"clock"},{id:"task",path:"/admin/tasks/:taskId",label:"Task",title:"Task",intro:"One scheduled task: its cadence, its switch and its own run history.",icon:"clock",hidden:!0},{id:"imports",path:"/admin/imports",label:"Imports",title:"Imports",intro:"Catalogue synchronisation history.",icon:"database"},{id:"runtime",path:"/admin/runtime",label:"Runtime",title:"Runtime",intro:"Goroutines, memory and the background workers inside the gateway process.",icon:"chip"},{id:"maintenance",path:"/admin/maintenance",label:"Maintenance",title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time.",icon:"wrench"},{id:"gateway-settings",path:"/admin/settings",label:"Gateway settings",title:"Gateway settings",intro:"Timezone, logging and the other server-level settings for this gateway.",icon:"sliders",hidden:!0}]},{id:"integrations",label:"Integrations",defaultCollapsed:!0,items:[{id:"integrations",path:"/admin/integrations",label:"Overview",title:"Integrations",intro:"Every external service Memby depends on: configured, working, and what it last did.",icon:"plug"},{id:"integration",path:"/admin/integrations/:integrationId",label:"Integration",title:"Integration",intro:"One external service: its switch, its settings, its jobs and its run history.",icon:"plug",hidden:!0},{id:"webhooks",path:"/admin/integrations/webhooks",label:"Event webhooks",title:"Event webhooks",intro:"Send administrative events to Discord and, in time, elsewhere.",icon:"send"}]},{id:"insights",label:"Insights",defaultCollapsed:!0,items:[{id:"views",path:"/admin/views",label:"Views",title:"App views",intro:"Home-screen visits, viewers and the times Memby is used.",icon:"overview"},{id:"searches",path:"/admin/searches",label:"Searches",title:"Searches",intro:"What viewers have been looking for, and what was searched just now.",icon:"search"},{id:"journeys",path:"/admin/journeys",label:"Journeys",title:"User journeys",intro:"How viewers move through Memby, use features and complete flows.",icon:"journey"},{id:"engagement",path:"/admin/engagement",label:"Row engagement",title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row.",icon:"chart"}]}],Gn=Oe.flatMap(s=>s.items),Zn=Oe.flatMap(s=>s.items.filter(n=>!n.path.includes(":")).map(n=>({...n,group:s.label??""})));class Is extends Error{constructor(n,t){super(n),this.status=t,this.name="ApiError"}}const Yn=5*60*1e3;let fn=Date.now();for(const s of["pointerdown","pointermove","keydown","wheel","scroll"])window.addEventListener(s,()=>{fn=Date.now()},{passive:!0});const Jn=()=>Date.now()-fn({}));throw new Is(a.error??`Request failed (${t.status})`,t.status)}if(t.status!==204)return await t.json()}function Ve(s){const n=new URLSearchParams;for(const[a,i]of Object.entries(s))i==null||i===""||i===!1||n.set(a,String(i));const t=n.toString();return t?`?${t}`:""}const D={get:s=>Xe(s),post:(s,n)=>Xe(s,{method:"POST",body:n===void 0?void 0:JSON.stringify(n)}),put:(s,n)=>Xe(s,{method:"PUT",body:n===void 0?void 0:JSON.stringify(n)}),del:s=>Xe(s,{method:"DELETE"})},Xn=3e4,yn=g.createContext(null);function et({children:s}){var h;const[n,t]=g.useState(),[a,i]=g.useState(!1),[l,r]=g.useState(""),[d,c]=g.useState(""),[o,u]=g.useState(!0),p=g.useRef(0),m=g.useCallback(async()=>{const j=++p.current;try{const y=await D.get("/admin/api/status");if(j!==p.current)return;t(y),i(!0),c("")}catch(y){if(j!==p.current)return;i(!1),c(y instanceof Error?y.message:String(y))}finally{j===p.current&&(r(new Date().toISOString()),u(!1))}},[]),x=g.useCallback(async j=>{var y;await D.post("/admin/api/maintenance",{enabled:j,message:((y=n==null?void 0:n.maintenance)==null?void 0:y.message)??""}),await m()},[m,(h=n==null?void 0:n.maintenance)==null?void 0:h.message]);g.useEffect(()=>{m();let j;const y=()=>{window.clearInterval(j),j=document.hidden?void 0:window.setInterval(()=>void m(),Xn)},v=()=>{y(),document.hidden||m()};return y(),document.addEventListener("visibilitychange",v),()=>{window.clearInterval(j),document.removeEventListener("visibilitychange",v)}},[m]);const f=g.useMemo(()=>{var j;return{status:n,version:(n==null?void 0:n.serverVersion)??"",currentUser:((j=n==null?void 0:n.currentUser)==null?void 0:j.trim())||"Administrator",online:a,checkedAt:l,error:d,loading:o,reload:m,setMaintenance:x}},[n,a,l,d,o,m,x]);return e.jsx(yn.Provider,{value:f,children:s})}function je(){const s=g.useContext(yn);if(!s)throw new Error("useGateway used outside GatewayProvider");return s}function st(s,n){const t=s.label.toLowerCase(),a=s.group.toLowerCase();return t.startsWith(n)?4:t.includes(n)?3:a.includes(n)?2:`${s.title} ${s.intro}`.toLowerCase().includes(n)?1:0}function nt(){const s=ws(),{status:n}=je(),[t,a]=g.useState(!1),[i,l]=g.useState(""),[r,d]=g.useState(0),c=g.useRef(null),o=g.useRef(null),u=g.useMemo(()=>{const m=i.trim().toLowerCase();return[...Zn,...((n==null?void 0:n.requestUsers)??[]).map(f=>({id:`user-${f.id}`,path:`/admin/accounts/${encodeURIComponent(f.id)}`,label:f.username||"Unnamed user",title:`User: ${f.username||"Unnamed user"}`,intro:"Open this user’s devices and settings.",group:"Users",icon:"people"})),...((n==null?void 0:n.clients)??[]).map(f=>({id:`device-${f.deviceId}`,path:`/admin/devices/${encodeURIComponent(f.deviceId)}`,label:f.deviceName||"Unnamed device",title:`Device: ${f.deviceName||"Unnamed device"}`,intro:`${f.username||"Unknown user"} · ${f.version||"unknown version"}`,group:"Devices",icon:"tv"}))].map((f,h)=>({item:f,rank:m?st(f,m):1,index:h})).filter(f=>f.rank>0).sort((f,h)=>h.rank-f.rank||f.index-h.index).map(({item:f,rank:h})=>({item:f,rank:h}))},[i,n]);g.useEffect(()=>d(0),[i]),g.useEffect(()=>{const m=x=>{var f;(f=c.current)!=null&&f.contains(x.target)||a(!1)};return document.addEventListener("pointerdown",m),()=>document.removeEventListener("pointerdown",m)},[]),g.useEffect(()=>{const m=x=>{var h,j;if(x.key!=="S"||!x.shiftKey||x.ctrlKey||x.metaKey||x.altKey)return;const f=document.activeElement;f&&(f.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(f.tagName))||(x.preventDefault(),(h=o.current)==null||h.focus(),(j=o.current)==null||j.select())};return window.addEventListener("keydown",m),()=>window.removeEventListener("keydown",m)},[]);const p=m=>{var x;a(!1),l(""),(x=o.current)==null||x.blur(),s(m)};return e.jsxs("div",{className:"omni",ref:c,"data-open":t||void 0,children:[e.jsxs("div",{className:"omni-input",children:[e.jsx(se,{name:"search"}),e.jsx("input",{ref:o,type:"search",value:i,placeholder:"Search","aria-label":"Search","aria-expanded":t,onFocus:()=>a(!0),onChange:m=>{l(m.target.value),a(!0)},onKeyDown:m=>{var x;if(m.key==="Escape")l(""),a(!1),(x=o.current)==null||x.blur();else if(m.key==="ArrowDown")m.preventDefault(),d(f=>Math.min(f+1,u.length-1));else if(m.key==="ArrowUp")m.preventDefault(),d(f=>Math.max(f-1,0));else if(m.key==="Enter"){const f=u[r];if(!f)return;m.preventDefault(),p(f.item.path)}}}),e.jsx("span",{className:"omni-key",children:"⇧S"})]}),t?e.jsx("div",{className:"omni-panel",role:"listbox",children:u.length===0?e.jsx("p",{className:"empty",children:"No pages, users or devices match that search."}):u.map((m,x)=>e.jsxs("a",{className:x===r?"omni-item on":"omni-item",href:m.item.path,role:"option","aria-selected":x===r,onPointerEnter:()=>d(x),onClick:f=>{f.preventDefault(),p(m.item.path)},children:[m.item.icon?e.jsx(se,{name:m.item.icon}):null,e.jsxs("span",{children:[e.jsx("b",{children:m.item.label}),e.jsx("small",{children:m.item.intro})]}),e.jsx("span",{className:"omni-group",children:m.item.group})]},m.item.id))}):null]})}const w=s=>(s??0).toLocaleString(),F=s=>s?new Date(s).toLocaleString():"—";function ye(s){if(!s)return"0s";if(s<1e3)return`${Math.round(s)}ms`;const n=Math.round(s/1e3);if(n<60)return`${n}s`;const t=Math.floor(n/60);return t<60?`${t}m ${n%60}s`:`${Math.floor(t/60)}h ${t%60}m`}function _e(s){const n=Math.round(Math.max(0,s??0)/6e4);if(n<=0)return"none";if(n<60)return`${n} min`;const t=Math.floor(n/60),a=n%60;return a===0?t===1?"1 hour":`${t} hours`:`${t}h ${a}m`}function Ne(s){if(!s||s<=0)return"on request only";if(s<60)return`every ${s}s`;const n=Math.round(s/60);if(n<60)return`every ${n} min`;const t=Math.round(n/60);return t<48?t===1?"hourly":`every ${t} hours`:`every ${Math.round(t/24)} days`}function he(s){const n=["B","KB","MB","GB"];let t=Number(s??0),a=0;for(;t>=1024&&aString(s??"?").trim().split(/\s+/).slice(0,2).map(n=>n[0]??"").join("").toUpperCase(),as=s=>`${Math.round((s??0)*100)}%`;function be(s){if(!s)return"—";const n=Date.now()-new Date(s).getTime();if(n<0)return"just now";const t=Math.floor(n/1e3);if(t<45)return"just now";const a=Math.floor(t/60);if(a<60)return`${a} min ago`;const i=Math.floor(a/60);if(i<24)return`${i}h ago`;const l=Math.floor(i/24);return l<30?`${l}d ago`:new Date(s).toLocaleDateString()}function ds(s){if(!s)return"—";const n=new Date(s).getTime()-Date.now();if(!Number.isFinite(n))return"—";if(n<=0)return"due now";const t=Math.round(n/1e3);if(t<60)return`in ${t}s`;const a=Math.round(t/60);if(a<60)return`in ${a} min`;const i=Math.round(a/60);return i<24?`in ${i}h`:`in ${Math.round(i/24)}d`}const kn=15*60*1e3,tt=3*60*60*1e3,Ye=s=>!!s&&Date.now()-new Date(s).getTime(){var y;try{const v=await D.get(`/admin/api/notifications?limit=${qs}`);t(v.events),i(v.unread),r(v.types),p.current=Math.max(p.current,((y=v.events[0])==null?void 0:y.id)??0),u("")}catch(v){u(v instanceof Error?v.message:String(v))}},[]),x=g.useCallback(y=>{p.current=Math.max(p.current,y.id),t(v=>v.some(b=>b.id===y.id)?v:[y,...v].sort((b,T)=>T.id-b.id).slice(0,qs)),y.readAt||i(v=>v+1),r(v=>v.some(b=>b.type===y.type)?v.map(b=>b.type===y.type?{...b,count:b.count+1}:b):[...v,{type:y.type,count:1}])},[]);g.useEffect(()=>{m()},[m]),g.useEffect(()=>{let y=null,v,b=!1;return(()=>{b||(y=new EventSource(`/admin/api/notifications/stream?after=${p.current}`),y.addEventListener("open",()=>{c(!0),window.clearInterval(v),v=void 0}),y.addEventListener("admin",k=>{try{x(JSON.parse(k.data))}catch{}}),y.addEventListener("error",()=>{c(!1),v===void 0&&(v=window.setInterval(()=>void m(),it))}))})(),()=>{b=!0,y==null||y.close(),window.clearInterval(v)}},[x,m]);const f=g.useCallback(async y=>{const v=y.filter(b=>b>0);if(v.length!==0){t(b=>b.map(T=>v.includes(T.id)&&!T.readAt?{...T,readAt:new Date().toISOString()}:T));try{const b=await D.post("/admin/api/notifications/read",{ids:v});i(b.unread)}catch{m()}}},[m]),h=g.useCallback(async()=>{i(0),t(y=>y.map(v=>v.readAt?v:{...v,readAt:new Date().toISOString()}));try{const y=await D.post("/admin/api/notifications/read",{all:!0});i(y.unread)}catch{m()}},[m]),j=g.useMemo(()=>({events:n,unread:a,types:l,connected:d,error:o,markRead:f,markAllRead:h,reload:m}),[n,a,l,d,o,f,h,m]);return e.jsx(Nn.Provider,{value:j,children:s})}function Ss(){const s=g.useContext(Nn);if(!s)throw new Error("useNotifications used outside NotificationProvider");return s}function Sn(s){return s.severity==="error"?"bad":s.severity==="warning"?"warn":"info"}function Cn(s){return s.startsWith("auth.")?"key":s.startsWith("device.")?"tv":s.startsWith("admin.")?"shield":s.startsWith("task.")?"clock":s.startsWith("integration.")?"plug":s.startsWith("library.")?"library":s.startsWith("emby.")?"globe":s.startsWith("server.")?"power":"bell"}function Ds(s){return{"auth.login":"Signed in","auth.login_failed":"Sign-in refused","auth.logout":"Signed out","device.registered":"New device","device.removed":"Device removed","device.renamed":"Device renamed","admin.sign_in":"Admin sign-in","server.started":"Server started","server.maintenance":"Maintenance","task.completed":"Task finished","task.failed":"Task failed","integration.failed":"Integration failed","integration.test":"Integration test","library.sync":"Library sync","emby.unreachable":"Emby unreachable","emby.recovered":"Emby recovered"}[s]??s.replace(/[._]/g," ")}const Fs=99;function rt(){const{events:s,unread:n,connected:t,markRead:a,markAllRead:i}=Ss(),[l,r]=g.useState(!1),d=g.useRef(null);return g.useEffect(()=>{const c=u=>{var p;(p=d.current)!=null&&p.contains(u.target)||r(!1)},o=u=>{u.key==="Escape"&&r(!1)};return document.addEventListener("pointerdown",c),document.addEventListener("keydown",o),()=>{document.removeEventListener("pointerdown",c),document.removeEventListener("keydown",o)}},[]),g.useEffect(()=>{if(!l)return;const c=s.filter(o=>!o.readAt).map(o=>o.id);c.length>0&&a(c)},[l]),e.jsxs("div",{className:"bell",ref:d,"data-open":l||void 0,children:[e.jsxs("button",{type:"button",className:"bell-button","aria-label":n>0?`Activity, ${n} unread`:"Activity","aria-expanded":l,onClick:()=>r(c=>!c),children:[e.jsx(se,{name:"bell"}),n>0?e.jsx("span",{className:"bell-badge",children:n>Fs?`${Fs}+`:n}):null]}),l?e.jsxs("div",{className:"bell-panel",children:[e.jsxs("div",{className:"bell-head",children:[e.jsx("b",{children:"Activity"}),e.jsxs("div",{className:"row tight",children:[t?null:e.jsx("span",{className:"tag","data-tone":"warn",children:"reconnecting"}),n>0?e.jsx("button",{type:"button","data-variant":"quiet","data-size":"sm",onClick:()=>void i(),children:"Mark all read"}):null]})]}),e.jsx("div",{className:"bell-list",children:s.length===0?e.jsx("p",{className:"empty",children:"Nothing has happened yet."}):s.slice(0,20).map(c=>{const o=e.jsxs(e.Fragment,{children:[e.jsx(Qe,{name:Cn(c.type),tone:Sn(c)}),e.jsxs("span",{className:"bell-body",children:[e.jsx("b",{children:c.title||c.type}),c.summary?e.jsx("p",{children:c.summary}):null,e.jsx("time",{dateTime:c.occurredAt,children:be(c.occurredAt)})]})]});return c.link?e.jsx(Z,{className:"bell-item","data-unread":!c.readAt||void 0,to:c.link,onClick:()=>r(!1),children:o},c.id):e.jsx("div",{className:"bell-item","data-unread":!c.readAt||void 0,children:o},c.id)})}),e.jsx("div",{className:"bell-foot",children:e.jsx(Z,{to:"/admin/activity",onClick:()=>r(!1),children:"All activity"})})]}):null]})}function J(s,n={}){const{pollMs:t,enabled:a=!0}=n,[i,l]=g.useState(),[r,d]=g.useState(""),[c,o]=g.useState(a),[u,p]=g.useState(!1),m=g.useRef(0),x=g.useRef(!1),f=g.useCallback(async()=>{if(!a)return;const h=++m.current;x.current&&p(!0);try{const j=await D.get(s);if(h!==m.current)return;l(j),d(""),x.current=!0}catch(j){if(h!==m.current)return;d(j instanceof Error?j.message:String(j))}finally{h===m.current&&(o(!1),p(!1))}},[s,a]);return g.useEffect(()=>(x.current=!1,o(!0),f(),()=>{m.current+=1}),[f]),g.useEffect(()=>{if(!t||!a)return;let h;const j=()=>{window.clearInterval(h),h=document.hidden?void 0:window.setInterval(()=>void f(),t)},y=()=>{j(),document.hidden||f()};return j(),document.addEventListener("visibilitychange",y),()=>{window.clearInterval(h),document.removeEventListener("visibilitychange",y)}},[t,a,f]),{data:i,error:r,loading:c,refreshing:u,reload:f,set:l}}function ne(){const[s,n]=g.useState(null),t=g.useRef(!0);g.useEffect(()=>()=>{t.current=!1},[]);const a=g.useCallback(async(i,l)=>{n(i);try{return await l(),!0}finally{t.current&&n(null)}},[]);return{busy:s,run:a}}const Os="Memby";function lt(s){g.useEffect(()=>{const n=s.trim();document.title=n?`${n} | ${Os}`:Os},[s])}function U({title:s,intro:n,actions:t,crumbs:a,icon:i}){var d;const l=ks(),r=i??((d=Gn.find(c=>vs({path:c.path,end:!0},l.pathname)))==null?void 0:d.icon);return lt(s),e.jsxs("header",{className:"page-head",children:[a?e.jsx("nav",{className:"crumbs",children:a}):null,e.jsxs("div",{className:"page-head-row",children:[e.jsxs("div",{className:"page-head-title",children:[r?e.jsx("span",{className:"page-head-icon","aria-hidden":"true",children:e.jsx(se,{name:r})}):null,e.jsxs("div",{className:"page-head-text",children:[e.jsx("h1",{children:s}),n?e.jsx("p",{children:n}):null]})]}),t?e.jsx("div",{className:"page-head-actions",children:t}):null]})]})}function M({title:s,intro:n,icon:t,tone:a,actions:i,footer:l,children:r}){return e.jsxs("section",{className:"card",children:[s?e.jsxs("div",{className:"card-head",children:[t?e.jsx(Qe,{name:t,tone:a}):null,e.jsxs("div",{className:"card-head-text",children:[e.jsx("h2",{children:s}),n?e.jsx("p",{children:n}):null]}),i?e.jsx("div",{className:"card-head-actions",children:i}):null]}):null,r,l?e.jsx("div",{className:"card-foot",children:l}):null]})}function xe({cols:s,children:n}){return e.jsx("div",{className:"grid","data-cols":s,children:n})}function ie({tiles:s}){return e.jsx("div",{className:"tiles",children:s.map(n=>e.jsxs("div",{className:"tile",children:[n.icon?e.jsx(Qe,{name:n.icon,tone:n.tone}):null,e.jsx("b",{className:n.small?"small":void 0,children:n.value}),e.jsx("span",{children:n.label})]},n.label))})}function N({children:s,tone:n}){return e.jsx("span",{className:"tag","data-tone":n,children:s})}function ce({children:s,tone:n}){return e.jsx("span",{className:"chip","data-tone":n,children:s})}function ee({children:s}){return e.jsx("p",{className:"empty",children:s})}function Y({columns:s,children:n}){return e.jsx("tr",{children:e.jsx("td",{colSpan:s,className:"muted",children:e.jsx("p",{className:"empty",children:n})})})}function pe({children:s,tone:n}){return e.jsx("p",{className:"note","data-tone":n,children:s})}function $({children:s,onClick:n,variant:t,size:a,disabled:i,busy:l,icon:r,type:d="button",title:c}){return e.jsxs("button",{type:d,className:"","data-variant":t,"data-size":a,disabled:i||l,onClick:n,title:c,children:[l?e.jsx("span",{className:"spinner"}):r?e.jsx(se,{name:r}):null,s]})}function q({label:s,hint:n,children:t,grow:a}){return e.jsxs("label",{className:a?"field grow":"field",children:[e.jsx("span",{children:s}),t,n?e.jsx("small",{children:n}):null]})}function G({label:s,hint:n,checked:t,onChange:a,disabled:i}){return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:t,disabled:i,onChange:l=>a(l.target.checked)}),e.jsx("span",{className:"switch"}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:s}),n?e.jsx("p",{children:n}):null]})]})}function Ue({value:s,options:n,onChange:t}){return e.jsx("div",{className:"segments",role:"group",children:n.map(a=>e.jsx("button",{type:"button","aria-pressed":a.value===s,onClick:()=>t(a.value),children:a.label},String(a.value)))})}function K({children:s}){return e.jsx("div",{className:"table-wrap",children:s})}function hs({data:s,labelOf:n,valueOf:t,toneOf:a,title:i}){if(s.length===0)return e.jsx(ee,{children:"Nothing in this window."});const l=s.map(d=>t(d)),r=Math.max(1,...l);return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"bars",children:s.map((d,c)=>{const o=t(d);return e.jsx("div",{className:"bar","data-tone":a==null?void 0:a(d),"data-empty":o===0||void 0,style:{height:`${Math.max(2,o/r*100)}%`},title:i?i(d):`${n(d,c)}: ${o}`},c)})}),e.jsxs("div",{className:"bars-axis",children:[e.jsx("span",{children:n(s[0],0)}),e.jsx("span",{children:n(s[s.length-1],s.length-1)})]})]})}function Mn({value:s,total:n,tone:t}){const a=n>0?Math.min(1,s/n):0;return e.jsx("div",{className:"meter","data-tone":t,children:e.jsx("div",{style:{width:`${a*100}%`}})})}function _({message:s,onDismiss:n}){return s?e.jsxs("div",{className:"banner",role:"alert",children:[e.jsx(se,{name:"alert"}),e.jsx("span",{children:s}),n?e.jsx("button",{type:"button",onClick:n,"aria-label":"Dismiss",children:e.jsx(se,{name:"close"})}):null]}):null}function V({rows:s=3}){return e.jsxs("div",{className:"loading-page","aria-busy":"true","aria-label":"Loading",children:[e.jsxs("div",{className:"loading-heading",children:[e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"})]}),e.jsx("div",{className:"loading-tiles",children:Array.from({length:4},(n,t)=>e.jsx("span",{className:"skeleton"},t))}),Array.from({length:s},(n,t)=>e.jsxs("section",{className:"loading-card",children:[e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"})]},t))]})}function ke({title:s,body:n,confirmLabel:t="Confirm",destructive:a,busy:i,onConfirm:l,onCancel:r}){const d=g.useId(),c=g.useRef(null);return g.useEffect(()=>{var u;(u=c.current)==null||u.focus();const o=p=>{p.key==="Escape"&&r()};return document.addEventListener("keydown",o),()=>document.removeEventListener("keydown",o)},[r]),e.jsx("div",{className:"scrim",onPointerDown:o=>o.target===o.currentTarget&&r(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true","aria-labelledby":d,ref:c,tabIndex:-1,children:[e.jsx("h2",{id:d,children:s}),e.jsx("p",{children:n}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{onClick:r,variant:"quiet",children:"Cancel"}),e.jsx($,{onClick:l,variant:a?"danger":"primary",busy:i,children:t})]})]})})}function We({rows:s}){return e.jsx("div",{className:"kv",children:s.map(n=>e.jsxs("div",{className:"kv-row",children:[e.jsx("span",{children:n.label}),e.jsx("span",{children:n.value})]},n.label))})}function xs({children:s,aside:n}){return e.jsxs("h4",{className:"subhead",children:[e.jsx("span",{children:s}),n?e.jsx("small",{children:n}):null]})}function Cs({tiles:s}){return e.jsx("div",{className:"tiles plain",children:s.map(n=>e.jsxs("div",{className:"tile",children:[e.jsx("b",{className:n.small?"small":void 0,children:n.value}),e.jsx("span",{children:n.label})]},n.label))})}function ot({open:s,onNavigate:n}){const{unread:t}=Ss(),a=ks(),[i,l]=g.useState(()=>{try{return JSON.parse(localStorage.getItem("memby-admin-nav")??"{}")}catch{return{}}}),r=c=>{try{localStorage.setItem("memby-admin-nav",JSON.stringify(c))}catch{}};g.useEffect(()=>{const c=Oe.find(o=>o.items.some(u=>vs({path:u.path,end:!0},a.pathname)));c&&l(o=>{const u={...o};return Oe.forEach(p=>{p.collapsible!==!1&&(u[p.id]=p.id!==c.id)}),r(u),u})},[a.pathname]);const d=(c,o=!1)=>{l(u=>{const p={...u},m=!(u[c]??o);return Oe.forEach(x=>{x.collapsible!==!1&&(p[x.id]=x.id===c?m:!0)}),r(p),p})};return e.jsx("nav",{className:"rail",id:"rail","data-open":s||void 0,"aria-label":"Console sections",children:Oe.map(c=>{const o=c.items.filter(x=>!x.hidden);if(o.length===0)return null;const u=o.some(x=>vs({path:x.path,end:!0},a.pathname)),p=c.collapsible!==!1,m=u||!p||!(i[c.id]??c.defaultCollapsed??!1);return e.jsxs("div",{className:"rail-group",children:[c.label&&p?e.jsxs("button",{type:"button",className:"rail-head","aria-expanded":m,onClick:()=>d(c.id,c.defaultCollapsed),children:[c.label,e.jsx(se,{name:"caret",className:"ico caret"})]}):c.label?e.jsx("div",{className:"rail-head rail-head-static",children:c.label}):null,m?o.map(x=>e.jsx(xn,{to:x.path,end:x.path==="/admin",onClick:n,className:({isActive:f})=>f?"on":"","aria-current":void 0,children:({isActive:f})=>e.jsxs("span",{style:{display:"contents"},ref:h=>{const j=h==null?void 0:h.parentElement;j&&(f?j.setAttribute("aria-current","page"):j.removeAttribute("aria-current"))},children:[x.icon?e.jsx(se,{name:x.icon}):null,x.label,x.badge==="notifications"&&t>0?e.jsx("span",{className:"rail-badge",children:t>99?"99+":t}):null]})},x.id)):null]},c.id)})})}function ct(){var k,I,E;const{version:s,currentUser:n,online:t,loading:a,status:i,setMaintenance:l}=je(),[r,d]=g.useState(!1),[c,o]=g.useState(!1),[u,p]=g.useState(!1),[m,x]=g.useState(!1),f=ks(),h=!!((k=i==null?void 0:i.maintenance)!=null&&k.enabled),j=!!((I=i==null?void 0:i.quietTime)!=null&&I.active),y=g.useRef(null),v=((E=Array.from(n.trim())[0])==null?void 0:E.toLocaleUpperCase("en-NZ"))||"A",b=i&&t&&!h&&!j?"ok":i||!a?"bad":"checking",T=async()=>{if(!(u||!i)){p(!0);try{await l(!h)}finally{p(!1)}}};return g.useEffect(()=>d(!1),[f.pathname]),g.useEffect(()=>{if(!c)return;const A=X=>{var le;(le=y.current)!=null&&le.contains(X.target)||o(!1)},L=X=>{X.key==="Escape"&&o(!1)};return document.addEventListener("mousedown",A),window.addEventListener("keydown",L),()=>{document.removeEventListener("mousedown",A),window.removeEventListener("keydown",L)}},[c]),g.useEffect(()=>{if(!r)return;const A=document.body.style.overflow;document.body.style.overflow="hidden";const L=X=>{X.key==="Escape"&&d(!1)};return window.addEventListener("keydown",L),()=>{document.body.style.overflow=A,window.removeEventListener("keydown",L)}},[r]),e.jsxs(e.Fragment,{children:[e.jsx("a",{className:"skip",href:"#main",children:"Skip to content"}),e.jsxs("header",{className:"topbar",children:[e.jsxs("a",{className:"topbar-brand",href:"/admin",children:[e.jsx("span",{className:"brand-mark","aria-hidden":"true",children:"M"}),e.jsx("span",{className:"brand-word",children:"Memby Gateway"})]}),e.jsx("button",{type:"button",className:"rail-toggle","aria-label":"Sections","aria-expanded":r,"aria-controls":"rail",onClick:()=>d(A=>!A),children:e.jsx(se,{name:"menu"})}),e.jsx("div",{className:"topbar-spacer"}),e.jsxs("div",{className:"topbar-tools",children:[e.jsx(nt,{}),e.jsxs("span",{className:"topbar-version",children:["gateway ",s||"unknown"]}),e.jsx("button",{type:"button",className:"topbar-status","data-tone":b,"aria-pressed":h,disabled:!i||!t||u||j,title:i?j?"Memby quiet time is active":h?"Bring Memby back online":t?"Take Memby offline":"Memby is not responding":"Checking Memby status","aria-label":j?"Memby quiet time is active":h?"Memby is offline. Bring it online":i&&t?"Memby is online. Take it offline":a?"Checking Memby status":"Memby is not responding",onClick:()=>h?void T():x(!0),children:e.jsx("span",{className:"dot","aria-hidden":"true"})}),e.jsx(rt,{}),e.jsxs("div",{className:"account-menu","data-open":c||void 0,ref:y,children:[e.jsxs("button",{type:"button",className:"account-trigger","aria-haspopup":"menu","aria-expanded":c,"aria-label":`Signed in as ${n}`,onClick:()=>o(A=>!A),children:[e.jsx("span",{className:"account-avatar","aria-hidden":"true",children:v}),e.jsx("span",{className:"account-name",children:n}),e.jsx(se,{name:"caret",className:"ico account-caret"})]}),c?e.jsxs("div",{className:"account-panel",role:"menu",children:[e.jsxs("div",{className:"account-identity",children:[e.jsx("span",{className:"account-avatar account-avatar-large","aria-hidden":"true",children:v}),e.jsxs("span",{children:[e.jsx("small",{children:"Signed in as"}),e.jsx("b",{children:n})]})]}),e.jsxs(xn,{to:"/admin/settings",role:"menuitem",onClick:()=>o(!1),children:[e.jsx(se,{name:"sliders"}),"Gateway settings"]}),e.jsx("form",{method:"post",action:"/admin/logout",children:e.jsxs("button",{type:"submit",role:"menuitem",children:[e.jsx(se,{name:"logout"}),"Log out"]})})]}):null]})]})]}),r?e.jsx("button",{type:"button",className:"rail-scrim","aria-label":"Close sections",onClick:()=>d(!1)}):null,e.jsx(ot,{open:r,onNavigate:()=>d(!1)}),e.jsx("main",{className:"page",id:"main",children:e.jsx(Pn,{})}),m?e.jsx(ke,{title:"Take Memby offline?",body:"Every television will stop working immediately. Viewers will see the maintenance message configured on the Maintenance page, while this console remains available.",confirmLabel:"Go offline",destructive:!0,busy:u,onConfirm:()=>{x(!1),T()},onCancel:()=>x(!1)}):null]})}const $n=g.createContext(null),dt=5e3;function ht({children:s}){const[n,t]=g.useState([]),a=g.useRef(1),i=g.useCallback(c=>{t(o=>o.filter(u=>u.id!==c))},[]),l=g.useCallback((c,o="ok")=>{const u=a.current++;t(p=>[...p,{id:u,message:c,tone:o}]),window.setTimeout(()=>i(u),dt)},[i]),r=g.useCallback(async(c,o)=>{try{const u=await c();return o&&l(o,"ok"),u}catch(u){l(u instanceof Error?u.message:String(u),"bad");return}},[l]),d=g.useMemo(()=>({show:l,wrap:r}),[l,r]);return e.jsxs($n.Provider,{value:d,children:[s,e.jsx("div",{className:"toasts",role:"status","aria-live":"polite",children:n.map(c=>e.jsxs("div",{className:"toast","data-tone":c.tone,children:[e.jsx(se,{name:c.tone==="bad"?"alert":"check"}),e.jsx("span",{children:c.message}),e.jsx("button",{type:"button",onClick:()=>i(c.id),"aria-label":"Dismiss",children:e.jsx(se,{name:"close"})})]},c.id))})]})}function ae(){const s=g.useContext($n);if(!s)throw new Error("useToast used outside ToastProvider");return s}const Ps={ok:"ok",watch:"warn",bad:"bad"},ut={ok:"Healthy",watch:"Worth watching",bad:"Needs attention"};function Rn({health:s,compact:n}){const t=s.notes??[];return e.jsxs("div",{className:"verdict","data-tone":Ps[s.level]??"ok",children:[e.jsxs("div",{className:"verdict-head",children:[e.jsx(se,{name:s.level==="ok"?"check":"alert"}),e.jsx("b",{children:ut[s.level]??"Unknown"}),e.jsx("span",{children:s.summary})]}),n||t.length===0?null:e.jsx("ul",{className:"verdict-notes",children:t.map((a,i)=>e.jsxs("li",{children:[e.jsx(N,{tone:Ps[a.level]??"note",children:a.area}),e.jsx("span",{children:a.message})]},`${a.area}-${i}`))})]})}function rs(s,n){switch(s.direction){case"rising":return`rising ${n(Math.abs(s.perHour))} an hour`;case"falling":return`falling ${n(Math.abs(s.perHour))} an hour`;case"steady":return"steady";default:return"gathering history"}}function Tn(s){if(!s||s<60)return`${Math.max(0,Math.round(s))}s`;const n=Math.floor(s/60);if(n<60)return`${n} min`;const t=Math.floor(n/60);return t<48?`${t}h ${n%60}m`:`${Math.floor(t/24)} days`}function mt(){var y,v,b,T,k,I;const{status:s,error:n,loading:t}=je(),a=J("/admin/api/runtime",{pollMs:3e4}),i=J("/admin/api/views",{pollMs:6e4});if(t||!s)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Overview",intro:"What the gateway is doing right now."}),e.jsx(_,{message:n}),e.jsx(V,{})]});const l=s.features??{features:[],revision:0,safeMode:!1},r=l.features??[],d=s.clients??[],c=d.filter(E=>Ye(E.lastSeen)).length,o=s.updatePolicy??{},u=!!o.minimumVersion&&o.minimumVersion===o.latestVersion,p=s.playbackPolicy,m=s.mdblist,x=s.forYou,f=(s.runs??[]).slice(0,5),h=a.data,j=h?h.memory.memoryLimit>0&&h.memory.memoryLimitE.enabled).length} / ${r.length}`,icon:"sliders",tone:"ok"},{label:"last import",value:F(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"What televisions are being told",intro:"The answers the gateway is giving every set right now.",icon:"tv",tone:"info",children:e.jsx(We,{rows:[{label:"Availability",value:(k=s.maintenance)!=null&&k.enabled?e.jsx(N,{tone:"bad",children:"offline for maintenance"}):(I=s.quietTime)!=null&&I.active?e.jsx(N,{tone:"warn",children:"quiet time active"}):e.jsx(N,{tone:"ok",children:"online"})},{label:"Feature control plane",value:l.safeMode?e.jsx(N,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(N,{tone:"ok",children:["revision r",w(l.revision)]})},{label:"App update prompt",value:o.enabled?e.jsxs(N,{tone:u?"warn":"ok",children:[u?"required · ":"optional · ",o.latestVersion]}):e.jsx(N,{children:"off"})},{label:"Catalogue import",value:s.syncRunning?e.jsx(N,{tone:"warn",children:"running"}):e.jsxs(N,{children:["every ",s.syncEvery]})},{label:"Playback preroll",value:(p==null?void 0:p.prerollEnabled)===!1?e.jsx(N,{children:"off"}):e.jsxs(N,{tone:"ok",children:[((p==null?void 0:p.prerollDurationMs)??6500)/1e3,"s"]})}]})}),e.jsx(M,{title:"Services",intro:"The services this gateway leans on, and whether they answered.",icon:"wrench",tone:"note",actions:e.jsx(Z,{to:"/admin/integrations",children:"Integrations"}),children:e.jsx(We,{rows:[{label:"Movies (Radarr)",value:s.radarrReady?e.jsx(N,{tone:"ok",children:"ready"}):e.jsx(N,{children:"not configured"})},{label:"Series (Sonarr)",value:s.sonarrReady?e.jsx(N,{tone:"ok",children:"ready"}):e.jsx(N,{children:"not configured"})},{label:"MDBList ratings",value:m!=null&&m.enabled?e.jsxs(N,{tone:"ok",children:[w(m.cachedTitles)," titles stored"]}):e.jsx(N,{children:m!=null&&m.apiKeyConfigured?"off · key saved":"off · no key"})},{label:"For You pools",value:s.forYouRunning?e.jsx(N,{tone:"warn",children:"rebuilding"}):e.jsxs(N,{children:[w((x==null?void 0:x.candidates)??0)," ranked candidates"]})},{label:"Recommendation profiles",value:e.jsx("span",{className:"mono",children:w((x==null?void 0:x.profiles)??0)})}]})})]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"Latest imports",intro:"The last few catalogue synchronisations.",icon:"sync",tone:"data",actions:e.jsx(Z,{to:"/admin/imports",children:"All imports"}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Started"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"num",children:"Written"})]})}),e.jsx("tbody",{children:f.length===0?e.jsx(Y,{columns:4,children:"No imports have run yet."}):f.map(E=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(E.startedAt)}),e.jsx("td",{children:E.kind}),e.jsx("td",{children:e.jsx(N,{tone:E.status==="success"?"ok":E.status==="running"?"warn":"bad",children:E.status})}),e.jsx("td",{className:"num",children:w(E.itemsUpserted)})]},E.id||E.startedAt))})]})})}),e.jsx(M,{title:"Process",intro:"The container the gateway is served from.",icon:"chip",tone:"info",actions:e.jsx(Z,{to:"/admin/runtime",children:e.jsx($,{size:"sm",variant:"quiet",icon:"external",children:"Runtime details"})}),children:h?e.jsxs(e.Fragment,{children:[e.jsx(Rn,{health:h.health,compact:!0}),e.jsx(Cs,{tiles:[{label:`heap used · ${rs(h.heapTrend,he)}`,value:he(h.memory.heapInuse)},{label:"of the process limit",value:j?`${(h.memory.heapShare*100).toFixed(1)}%`:"no limit"},{label:`goroutines · ${rs(h.goroutineTrend,E=>w(Math.round(E)))}`,value:w(h.goroutines)},{label:"running for",value:Tn(h.uptimeSeconds)}]}),e.jsxs("p",{className:"hint",children:["Heap in use is what the gateway is holding;"," ",he(h.memory.sys)," is reserved from the operating system on its behalf, which is always larger and is not a leak.",j?` The limit is ${he(h.memory.memoryLimit)}${h.memory.configuredLimit?" (GOMEMLIMIT)":""}.`:" No memory limit is set."]})]}):e.jsx("p",{className:"hint",children:"Reading process statistics…"})})]})]})}const pt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"}];function xt(){const{unread:s,connected:n,markAllRead:t,reload:a}=Ss(),[i,l]=g.useState(7),[r,d]=g.useState(""),[c,o]=g.useState(""),[u,p]=g.useState(!1),[m,x]=g.useState(0),f=100,h=g.useMemo(()=>`/admin/api/notifications${Ve({days:i,type:r,severity:c,unread:u,limit:f,offset:m*f})}`,[i,r,c,u,m]),{data:j,error:y,loading:v,reload:b}=J(h),T=async()=>{await t(),await b()};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Activity",intro:"Every administrative event the gateway has published: sign-ins, devices, scheduled tasks, integrations and the server itself. The same feed the bell and every integration read from.",actions:s>0?e.jsx($,{onClick:()=>void T(),icon:"check",children:"Mark all read"}):void 0}),e.jsx(_,{message:y}),e.jsx(ie,{tiles:[{label:"Events in window",value:w((j==null?void 0:j.total)??0),icon:"bell",tone:"info"},{label:"Unread",value:w(s),icon:"alert",tone:s>0?"warn":void 0},{label:"Kinds seen",value:w((j==null?void 0:j.types.length)??0),icon:"list",tone:"note"},{label:"Live feed",value:n?"connected":"reconnecting",small:!0,icon:"pulse",tone:n?"ok":"warn"}]}),e.jsxs("div",{className:"filters",children:[e.jsx(q,{label:"Window",children:e.jsx(Ue,{value:i,options:pt.map(k=>({value:k.value,label:k.label})),onChange:k=>{l(k),x(0)}})}),e.jsx(q,{label:"Kind",children:e.jsxs("select",{value:r,onChange:k=>{d(k.target.value),x(0)},children:[e.jsx("option",{value:"",children:"Everything"}),((j==null?void 0:j.types)??[]).map(k=>e.jsxs("option",{value:k.type,children:[Ds(k.type)," (",k.count,")"]},k.type))]})}),e.jsx(q,{label:"Severity",children:e.jsxs("select",{value:c,onChange:k=>{o(k.target.value),x(0)},children:[e.jsx("option",{value:"",children:"Any"}),e.jsx("option",{value:"info",children:"Information"}),e.jsx("option",{value:"warning",children:"Warning"}),e.jsx("option",{value:"error",children:"Error"})]})}),e.jsx(q,{label:"Read state",children:e.jsxs("select",{value:u?"unread":"",onChange:k=>{p(k.target.value==="unread"),x(0)},children:[e.jsx("option",{value:"",children:"All"}),e.jsx("option",{value:"unread",children:"Unread only"})]})}),e.jsx("div",{className:"filter-actions",children:e.jsx($,{variant:"quiet",size:"sm",icon:"refresh",onClick:()=>{b(),a()},children:"Refresh"})})]}),v?e.jsx(V,{}):e.jsx(M,{title:"Events",icon:"bell",tone:"info",footer:((j==null?void 0:j.total)??0)>f?e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",disabled:m===0,onClick:()=>x(m-1),children:"Newer"}),e.jsx($,{size:"sm",disabled:(m+1)*f>=((j==null?void 0:j.total)??0),onClick:()=>x(m+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"What happened"}),e.jsx("th",{children:"Who"}),e.jsx("th",{children:"What"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:((j==null?void 0:j.events.length)??0)===0?e.jsx(Y,{columns:6,children:"Nothing has happened in this window."}):j==null?void 0:j.events.map(k=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(k.occurredAt),children:be(k.occurredAt)}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(Qe,{name:Cn(k.type),tone:Sn(k)}),Ds(k.type)]})}),e.jsxs("td",{children:[e.jsx("b",{children:k.title}),k.summary?e.jsx("div",{className:"muted",children:k.summary}):null]}),e.jsx("td",{className:"muted nowrap",children:k.actor||"—"}),e.jsx("td",{className:"muted nowrap",children:k.target||"—"}),e.jsxs("td",{className:"nowrap",children:[k.readAt?null:e.jsx(N,{tone:"ok",children:"new"}),k.link?e.jsx(Z,{className:"table-row-link",to:k.link,children:"Open"}):null]})]},k.id))})]})})})]})}function _s(s){const n=s?new Date(s).getTime():0;return Number.isFinite(n)?n:0}function Us(){return e.jsx("span",{className:"muted",title:"No Tracearr sessions matched to this person",children:"—"})}function jt(){const{data:s,error:n,loading:t}=J("/admin/api/accounts",{pollMs:6e4}),a=(s==null?void 0:s.accounts)??[],i=a.flatMap(u=>u.devices??[]),l=a.filter(u=>{var p;return(p=u.recommendations)==null?void 0:p.completed}).length,r=a.filter(u=>{var p,m;return((p=u.recommendations)==null?void 0:p.prompted)&&!((m=u.recommendations)!=null&&m.completed)}).length,d=a.filter(u=>{var p;return(p=u.watchTime)==null?void 0:p.matched}),c=d.reduce((u,p)=>{var m;return u+(((m=p.watchTime)==null?void 0:m.weekMs)??0)},0),o=[...a].sort((u,p)=>_s(p.lastSeen)-_s(u.lastSeen));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Users",intro:"Who uses Memby, and which devices they are signed in to."}),e.jsx(_,{message:n}),t?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"Memby users",value:w(a.length),icon:"people",tone:"note"},{label:"signed-in devices",value:w(i.length),icon:"tv",tone:"info"},{label:"active in the last 15 mins",value:w(i.filter(u=>Ye(u.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"recommendation setups completed",value:w(l),icon:"check",tone:"ok"},{label:"setup prompts queued",value:w(r),icon:"sparkle",tone:"note"},...d.length?[{label:"watch time this week",value:_e(c),icon:"pulse",tone:"data"}]:[]]}),e.jsx(M,{title:"Users",icon:"people",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"User"}),e.jsx("th",{children:"Short name"}),e.jsx("th",{className:"num",children:"Devices"}),e.jsx("th",{className:"num",children:"This week"}),e.jsx("th",{className:"num",children:"This month"}),e.jsx("th",{children:"Recommendations"}),e.jsx("th",{children:"Last seen"})]})}),e.jsx("tbody",{children:o.length===0?e.jsx(Y,{columns:7,children:"No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here."}):o.map(u=>{var j,y;const p=u.devices??[],m=p.filter(v=>Ye(v.lastSeen)).length,x=(j=u.recommendations)!=null&&j.completed?{label:"personalised",tone:"ok"}:(y=u.recommendations)!=null&&y.prompted?{label:"prompt queued",tone:"warn"}:{label:"not invited",tone:void 0},f=Ns(u.lastSeen),h=u.watchTime;return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":f.tone,title:f.label}),e.jsx("span",{className:"avatar",children:u.initials||wn(u.username)}),e.jsx(Z,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(u.id)}`,children:u.username||"Unnamed user"})]})}),e.jsx("td",{children:u.shortName||e.jsx("span",{className:"muted",title:"Memby greets them by their account name",children:"account name"})}),e.jsxs("td",{className:"num",children:[w(p.length),m?e.jsxs("span",{className:"table-sub",children:[w(m)," active now"]}):null]}),e.jsx("td",{className:"num",children:h!=null&&h.matched?_e(h.weekMs):e.jsx(Us,{})}),e.jsx("td",{className:"num muted",children:h!=null&&h.matched?_e(h.monthMs):e.jsx(Us,{})}),e.jsx("td",{children:e.jsx(N,{tone:x.tone,children:x.label})}),e.jsx("td",{className:"nowrap muted",title:F(u.lastSeen),children:be(u.lastSeen)})]},u.id)})})]})})})]})]})}function js(s){const n=String(s??"").replace("#","");return n.length!==8?`#${n}`:`#${n.slice(2)}${n.slice(0,2)}`}function gt(){var z,oe,we,Re,Te,ge;const{userId:s=""}=Be(),n=ws(),{wrap:t}=ae(),{busy:a,run:i}=ne(),l=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:r,error:d,loading:c,reload:o}=J("/admin/api/accounts",{pollMs:3e4}),[u,p]=g.useState(null),[m,x]=g.useState(null),[f,h]=g.useState(null),[j,y]=g.useState(null),[v,b]=g.useState(null),[T,k]=g.useState(null),I=J(`${l}/viewers`),E=((r==null?void 0:r.accounts)??[]).find(R=>R.id===s),A=(((z=I.data)==null?void 0:z.viewers)??[]).filter(R=>R.kind!=="main"),L=!!((oe=I.data)!=null&&oe.enabled)&&A.length<(((we=I.data)==null?void 0:we.maxShadowViewers)??0),X=(r==null?void 0:r.catalogue)??[],le=(r==null?void 0:r.themes)??[];g.useEffect(()=>{var R;u===null&&E&&p({...((R=E.settings)==null?void 0:R.preferences)??{}})},[E,u]),g.useEffect(()=>{f===null&&E&&h({...E.notifications})},[E,f]),g.useEffect(()=>{if(m!==null||!E)return;const R=E.themes??[];x(R.length===0?le.map(P=>P.id):R)},[E,m,le]);const ue=g.useMemo(()=>{const R=[];for(const P of X){let de=R.find(me=>me.name===P.area);de||R.push(de={name:P.area,definitions:[]}),de.definitions.push(P)}return R},[X]),te=(R,P,de,me)=>i(R,async()=>{const Ie=await t(P,de);y(null),Ie!==void 0&&(me==null||me()),await o()});if(c)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(Z,{to:"/admin/accounts",children:"← All users"})}),e.jsx(V,{})]});if(!E)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(Z,{to:"/admin/accounts",children:"← All users"})}),e.jsx(_,{message:d}),e.jsx(M,{children:e.jsx(ee,{children:"This user is no longer signed in to Memby."})})]});const H=E.devices??[],re=H.filter(R=>Ye(R.lastSeen)).length,S=E.settings??{},O=E.recommendations??{};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:E.username||"Unnamed user",intro:`Memby user · ${E.shortName?`greeted as ${E.shortName} · `:""}${w(H.length)} device${H.length===1?"":"s"} · last seen ${F(E.lastSeen)}`,crumbs:e.jsx(Z,{to:"/admin/accounts",children:"← All users"}),actions:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"avatar",children:E.initials||wn(E.username)}),re?e.jsxs(N,{tone:"ok",children:[re," active now"]}):e.jsx(N,{children:"idle"}),e.jsx(ce,{children:E.id})]})}),e.jsx(_,{message:d}),e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Devices",intro:"Every build a set has been seen running is listed under it. Signing one out revokes its Memby session, drops that history and removes it from Emby's own device list. Its Emby account is not changed.",icon:"tv",tone:"info",children:H.length===0?e.jsx(ee,{children:"No devices are signed in to this user."}):e.jsx("div",{className:"list",children:H.map(R=>{const P=Ns(R.lastSeen);return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[e.jsx("span",{className:"dot-state","data-tone":P.tone,title:P.label})," ",e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(R.id)}`,children:R.name||"Memby TV"})]}),e.jsxs("p",{children:[R.version?`Memby ${R.version}`:"Legacy Memby client"," · ",P.label," · last seen ",F(R.lastSeen)," · signed in ",F(R.signedInAt)]}),(R.versions??[]).length>0?e.jsx("div",{className:"chips",children:(R.versions??[]).map(de=>e.jsxs(ce,{tone:de.version===R.version?"ok":void 0,children:[de.version,de.version===R.version?" · now":""]},de.version))}):null]}),e.jsxs("div",{className:"list-actions",children:[e.jsx($,{size:"sm",disabled:!R.id,onClick:()=>b({id:R.id,name:R.name}),children:"Rename"}),e.jsx($,{size:"sm",variant:"danger",disabled:!R.id,onClick:()=>y({kind:"remove-device",deviceId:R.id,name:R.name}),children:"Sign out"})]})]},R.id||R.name)})})}),e.jsxs(M,{title:"Viewers",intro:"Several people under one Emby account, each with their own Continue Watching, watched history and favourites. Only the account itself is synced with Emby; a shadow viewer's watching is kept by Memby and never reported.",icon:"people",tone:"note",actions:I.data&&!I.data.enabled?e.jsx(N,{tone:"warn",children:"switched off"}):e.jsxs(N,{tone:"ok",children:[w(A.length)," beside the account"]}),footer:L?e.jsx($,{variant:"primary",onClick:()=>k({id:null,name:""}),children:"Add viewer"}):e.jsx($,{disabled:!0,children:I.data?`${w(I.data.maxShadowViewers)} is the limit`:"Add viewer"}),children:[I.data&&!I.data.enabled?e.jsx(_,{message:"Viewers are switched off for this server, so every television watches as the account. Nothing here is deleted — turn the feature on under Features to bring these people back."}):null,I.loading?e.jsx(V,{}):(((Re=I.data)==null?void 0:Re.viewers)??[]).length===0?e.jsx(ee,{children:"Nobody is set up yet, so this account has one viewer: itself."}):e.jsx("div",{className:"list",children:(((Te=I.data)==null?void 0:Te.viewers)??[]).map(R=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:R.name}),e.jsxs("p",{children:[R.kind==="main"?"The account itself — named by Emby, and the only one whose watching Emby hears about":"Watches privately; nothing reaches Emby",R.createdAt&&R.kind!=="main"?` · added ${F(R.createdAt)}`:""]})]}),e.jsx("div",{className:"list-actions",children:R.kind==="main"?e.jsx(N,{tone:"info",children:"synced with Emby"}):e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",onClick:()=>k({id:R.id,name:R.name}),children:"Rename"}),e.jsx($,{size:"sm",variant:"danger",onClick:()=>y({kind:"remove-viewer",viewer:R}),children:"Remove"})]})})]},R.id))})]}),e.jsxs(M,{title:"Recommendation setup",intro:"The prompt appears the next time this person opens Memby on any of their televisions.",icon:"sparkle",tone:"note",footer:O.completed?e.jsx($,{busy:a==="reset-rec",onClick:()=>y({kind:"reset-recommendations"}),children:"Clear stored choices"}):O.prompted?e.jsx($,{onClick:()=>y({kind:"cancel-prompt"}),children:"Cancel prompt"}):e.jsx($,{variant:"primary",busy:a==="prompt",onClick:()=>void te("prompt",()=>D.put(`${l}/recommendations/prompt`),"Setup prompt queued."),children:"Send setup prompt"}),children:[e.jsx("div",{className:"row tight",children:O.completed?e.jsx(N,{tone:"ok",children:"completed"}):O.prompted?e.jsx(N,{tone:"warn",children:"prompt queued"}):e.jsx(N,{children:"not invited"})}),e.jsx(vt,{prompt:O})]})]}),(ge=E.watchTime)!=null&&ge.matched?e.jsx(M,{title:"Watch time",intro:"From Tracearr, for this person across every client — not only Memby. The week runs from Monday and the month from the first, both in the server's own timezone.",icon:"pulse",tone:"data",actions:E.watchTime.tracearrUsername?e.jsx(ce,{children:E.watchTime.tracearrUsername}):null,children:e.jsx(ie,{tiles:[{label:`this week · ${w(E.watchTime.weekSessions)} session${E.watchTime.weekSessions===1?"":"s"}`,value:_e(E.watchTime.weekMs),icon:"pulse",tone:"data"},{label:`this month · ${w(E.watchTime.monthSessions)} session${E.watchTime.monthSessions===1?"":"s"}`,value:_e(E.watchTime.monthMs),icon:"calendar",tone:"info"},{label:"since Tracearr started recording",value:_e(E.watchTime.totalMs),icon:"clock",tone:"note"},{label:"last watched",value:F(E.watchTime.lastWatchedAt),icon:"history",tone:void 0,small:!0}]})}):null,e.jsx(M,{title:"Notifications",intro:"Choose what this person sees across every television. Changes apply through the gateway within a few seconds and do not require an app release.",icon:"bell",tone:"note",actions:f!=null&&f.enabled?e.jsx(N,{tone:"ok",children:"enabled"}):e.jsx(N,{children:"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:a==="notifications",onClick:()=>void te("notifications",()=>D.put(`${l}/notifications`,f),"Notification settings saved.",()=>h(null)),children:"Save notifications"}),e.jsx($,{onClick:()=>{h(null),o()},children:"Discard changes"})]}),children:f?e.jsxs("div",{className:"checks columns",children:[e.jsx(G,{label:"All notifications",hint:"The master switch. Turning this off hides every optional notification below.",checked:f.enabled,onChange:R=>h(P=>P&&{...P,enabled:R})}),e.jsx(G,{label:"My Shows return dates",hint:"Remind this person when a followed show is about to return.",checked:f.showReturnAlerts,disabled:!f.enabled,onChange:R=>h(P=>P&&{...P,showReturnAlerts:R})}),e.jsx(G,{label:"Sonarr television alerts",hint:"New episodes, additions and cancellation news supplied by Sonarr.",checked:f.sonarrAlerts,disabled:!f.enabled,onChange:R=>h(P=>P&&{...P,sonarrAlerts:R})}),e.jsx(G,{label:"Radarr film alerts",hint:"Notify this person when Radarr imports a new film.",checked:f.radarrAlerts,disabled:!f.enabled,onChange:R=>h(P=>P&&{...P,radarrAlerts:R})}),e.jsx(G,{label:"Optional app updates",hint:"Offer new app versions to this person. Mandatory compatibility updates are always enforced.",checked:f.updateAlerts,disabled:!f.enabled,onChange:R=>h(P=>P&&{...P,updateAlerts:R})}),e.jsx(G,{label:"Library activity",hint:"Show alerts after the Memby library catalogue is refreshed.",checked:f.libraryAlerts,disabled:!f.enabled,onChange:R=>h(P=>P&&{...P,libraryAlerts:R})}),e.jsx(G,{label:"Weekly watch-time summary",hint:"Send this person their week-to-date and month-to-date viewing on Sunday evening, and a summary of the month just gone once it ends. Needs Tracearr.",checked:f.watchTimeDigest,disabled:!f.enabled,onChange:R=>h(P=>P&&{...P,watchTimeDigest:R})}),e.jsx(G,{label:"Service status",hint:"Memby deployment and Emby outage or recovery notices. Maintenance mode itself still applies.",checked:f.systemAlerts,disabled:!f.enabled,onChange:R=>h(P=>P&&{...P,systemAlerts:R})})]}):null}),e.jsx(M,{title:"Settings",intro:"These live on the server and follow the person, so a change here reaches every television they use — usually within a few seconds, and on the next launch for a set that is switched off.",icon:"sliders",tone:"ok",actions:S.saved?e.jsxs(N,{tone:S.source==="admin"?"warn":"ok",children:["r",w(S.revision)," · ",S.source||"device"," · ",F(S.updatedAt)]}):e.jsx(N,{children:"defaults · never synced"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:a==="push",onClick:()=>void te("push",()=>D.put(`${l}/preferences`,{preferences:u??{}}),"Pushed to their televisions.",()=>p(null)),children:"Push to their televisions"}),e.jsx($,{onClick:()=>{p(null),o()},children:"Discard changes"}),e.jsx($,{onClick:()=>y({kind:"reset-preferences"}),children:"Restore defaults"}),e.jsx(Z,{className:"crumb",to:`/admin/accounts/${encodeURIComponent(s)}/settings`,children:"History and rollback →"})]}),children:ue.map(R=>e.jsxs("div",{className:"group",children:[e.jsx("p",{className:"group-label",children:R.name}),R.definitions.map(P=>e.jsx(bt,{definition:P,value:u==null?void 0:u[P.key],onChange:de=>p(me=>({...me??{},[P.key]:de}))},P.key))]},R.name))}),e.jsx(M,{title:"Colour schemes",intro:"Which palettes this person may choose between in Settings → Appearance. Tick everything to leave them unrestricted. Their current choice is an ordinary setting above; withdrawing it here puts them back on Midnight.",icon:"sparkle",tone:"note",actions:(E.themes??[]).length===0?e.jsx(N,{children:"all schemes"}):e.jsxs(N,{tone:"note",children:[w((E.themes??[]).length)," of ",w(le.length)]}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:a==="themes",onClick:()=>(m??[]).length===0?y({kind:"no-themes"}):void te("themes",()=>D.put(`${l}/themes`,{themes:m??[]}),"Colour schemes saved.",()=>x(null)),children:"Save colour schemes"}),e.jsx($,{onClick:()=>x(le.map(R=>R.id)),children:"Allow all"}),e.jsxs("span",{className:"hint",children:["Seasonal themes are not listed. They apply to every television in the house for their dates and nobody can decline one — the only switch is ",e.jsx("em",{children:"Seasonal themes"})," on the features page."]})]}),children:e.jsx("div",{className:"checks columns",children:le.map(R=>{var P,de,me;return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:(m??[]).includes(R.id),onChange:Ie=>x(He=>Ie.target.checked?[...He??[],R.id]:(He??[]).filter(C=>C!==R.id))}),e.jsx("span",{className:"switch"}),e.jsx("span",{className:"swatch",style:{"--swatch-surface":js((P=R.palette)==null?void 0:P.surface),"--swatch-accent":js((de=R.palette)==null?void 0:de.accent),"--swatch-hairline":js((me=R.palette)==null?void 0:me.hairline)},children:e.jsx("i",{})}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:R.name}),e.jsx("p",{children:R.description})]})]},R.id)})})}),e.jsx(M,{title:"Remove Memby access",intro:"Signs every one of this person's Memby devices out. Their Emby account, viewing history and library permissions are untouched.",icon:"alert",tone:"bad",children:e.jsx($,{variant:"danger",onClick:()=>y({kind:"remove-account"}),children:"Remove Memby access"})}),v?e.jsx(ft,{initial:v.name,busy:a==="rename",onCancel:()=>b(null),onConfirm:R=>void te("rename",()=>D.put(`${l}/devices/${encodeURIComponent(v.id)}`,{deviceName:R}),"Device renamed.",()=>b(null))}):null,T?e.jsx(yt,{initial:T.name,renaming:T.id!==null,busy:a==="viewer-name",onCancel:()=>k(null),onConfirm:R=>void te("viewer-name",()=>T.id?D.put(`${l}/viewers/${encodeURIComponent(T.id)}`,{name:R}):D.post(`${l}/viewers`,{name:R}),T.id?"Viewer renamed.":"Viewer added.",()=>{k(null),I.reload()})}):null,j?e.jsx(wt,{pending:j,busy:a,username:E.username,onCancel:()=>y(null),onConfirm:()=>{switch(j.kind){case"remove-device":return void te("remove-device",()=>D.del(`${l}/devices/${encodeURIComponent(j.deviceId)}`),"Device signed out.");case"remove-viewer":return void te("remove-viewer",()=>D.del(`${l}/viewers/${encodeURIComponent(j.viewer.id)}`),"Viewer removed.",()=>void I.reload());case"remove-account":return void te("remove-account",()=>D.del(`${l}/sessions`),"Memby access removed.",()=>n("/admin/accounts"));case"reset-recommendations":case"cancel-prompt":return void te("reset-rec",()=>D.del(`${l}/recommendations`),"Recommendation choices cleared.");case"reset-preferences":return void te("reset-prefs",()=>D.del(`${l}/preferences`),"Defaults restored.",()=>p(null));case"no-themes":return void te("themes",()=>D.put(`${l}/themes`,{themes:[]}),"Colour schemes saved.",()=>x(null))}}}):null]})}function vt({prompt:s}){const n=s.ratings??[],t=[["Genres",s.genres],["Studios",s.studios],["Actors",s.actors],["Actresses",s.actresses],["Directors",s.directors],["Types",s.contentTypes]],a=[...n.map(i=>e.jsxs(ce,{tone:"warn",children:[i.title," · ",w(i.rating)," ★"]},`r:${i.title}`)),...t.flatMap(([i,l])=>(l??[]).map(r=>e.jsxs(ce,{children:[i,": ",r]},`${i}:${r}`)))];return a.length===0?e.jsx(ee,{children:"No recommendation selections have been saved."}):e.jsx("div",{className:"chips",children:a})}function bt({definition:s,value:n,onChange:t}){if(s.kind==="toggle")return e.jsx(G,{label:s.name,hint:s.description,checked:!!n,onChange:t});if(s.kind==="choice"||s.kind==="number"){const i=s.unit??"",l=s.kind==="number"?(s.numbers??[]).map(r=>({value:String(r),label:r===0?"No limit":i?`${r} ${i}`:String(r)})):s.options??[];return e.jsx(q,{label:s.name,hint:s.description,children:e.jsx("select",{value:String(n??""),onChange:r=>t(s.kind==="number"?Number(r.target.value):r.target.value),children:l.map(r=>e.jsx("option",{value:r.value,children:r.label},r.value))})})}if(s.kind==="multi"){const i=Array.isArray(n)?n:[],l=[...i,...(s.options??[]).map(r=>r.value).filter(r=>!i.includes(r))];return e.jsxs("div",{className:"field",children:[e.jsx("span",{children:s.name}),e.jsx("small",{children:s.description}),e.jsx("div",{className:"checks",children:l.map(r=>{const d=(s.options??[]).find(c=>c.value===r);return d?e.jsx(G,{label:d.label,checked:i.includes(r),onChange:c=>t(c?[...i,r]:i.filter(o=>o!==r))},r):null})})]})}if(s.kind==="text")return e.jsx(q,{label:s.name,hint:s.description,children:e.jsx("input",{type:"text",value:String(n??""),maxLength:s.maxLength,placeholder:s.placeholder,onChange:i=>t(s.uppercase?i.target.value.toLocaleUpperCase("en-NZ"):i.target.value)})});const a=Array.isArray(n)?n:[];return e.jsx(q,{label:s.name,hint:s.description,children:e.jsx("textarea",{spellCheck:!1,placeholder:"One row id per line",value:a.join(` -`),onChange:i=>t(i.target.value.split(` -`).map(l=>l.trim()).filter(Boolean))})})}function ft({initial:s,busy:n,onConfirm:t,onCancel:a}){const[i,l]=g.useState(s||"Memby TV");return e.jsx("div",{className:"scrim",onPointerDown:r=>r.target===r.currentTarget&&a(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:"Name this device"}),e.jsx("p",{children:"The name a viewer sees in Settings → Devices, and what the console calls it."}),e.jsx(q,{label:"Device name",children:e.jsx("input",{type:"text",value:i,autoFocus:!0,maxLength:80,onChange:r=>l(r.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:a,children:"Cancel"}),e.jsx($,{variant:"primary",busy:n,disabled:!i.trim(),onClick:()=>t(i.trim()),children:"Rename"})]})]})})}function yt({initial:s,renaming:n,busy:t,onConfirm:a,onCancel:i}){const[l,r]=g.useState(s);return e.jsx("div",{className:"scrim",onPointerDown:d=>d.target===d.currentTarget&&i(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:n?"Rename this viewer":"Add a viewer"}),e.jsx("p",{children:"The name shown on the television’s “Who’s watching?” screen. Everything they watch is kept by Memby and never reported to Emby."}),e.jsx(q,{label:"Name",children:e.jsx("input",{type:"text",value:l,autoFocus:!0,maxLength:40,onChange:d=>r(d.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:i,children:"Cancel"}),e.jsx($,{variant:"primary",busy:t,disabled:!l.trim(),onClick:()=>a(l.trim()),children:n?"Rename":"Add viewer"})]})]})})}function wt({pending:s,busy:n,username:t,onConfirm:a,onCancel:i}){const r={"remove-device":{title:"Sign this device out of Memby?",body:"Its Emby account will not be changed. The set can sign in again at any time.",label:"Sign out",destructive:!0},"remove-account":{title:`Remove Memby access for ${t||"this user"}?`,body:"Every Memby device will be signed out. Their Emby account, viewing history and library permissions are untouched.",label:"Remove access",destructive:!0},"reset-recommendations":{title:"Clear this person's stored recommendation choices?",body:"Viewing history remains intact; only the explicit setup answers are removed.",label:"Clear",destructive:!0},"cancel-prompt":{title:"Cancel this person's queued recommendation prompt?",body:"They will not be invited to set up recommendations on their next launch.",label:"Cancel prompt",destructive:!1},"reset-preferences":{title:"Restore the Memby defaults for this person?",body:"Their televisions will pick the change up the next time they check in.",label:"Restore defaults",destructive:!0},"remove-viewer":{title:"Remove this viewer?",body:"What they were part-way through, what they had watched and their favourites are deleted, on every television in the house. The Emby account is untouched.",label:"Remove viewer",destructive:!0},"no-themes":{title:"Allow this person no colour schemes?",body:"They will be left on Midnight with nothing to choose between.",label:"Save anyway",destructive:!0}}[s.kind];return e.jsx(ke,{title:r.title,body:r.body,confirmLabel:r.label,destructive:r.destructive,busy:!!n,onConfirm:a,onCancel:i})}function kt(s,n){if(s.kind==="toggle")return n?"On":"Off";if(s.kind==="choice"){const a=(s.options??[]).find(i=>i.value===n);return a?a.label:String(n??"")}if(s.kind==="number")return Number(n)===0&&s.unit?"No limit":s.unit?`${n} ${s.unit}`:String(n??"");const t=Array.isArray(n)?n:[];return t.length===0?"None":t.map(a=>{var i;return((i=(s.options??[]).find(l=>l.value===a))==null?void 0:i.label)??a}).join(", ")}function Nt(){const{userId:s=""}=Be(),{wrap:n}=ae(),{busy:t,run:a}=ne(),i=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:l,error:r,loading:d,reload:c}=J(`${i}/preferences/history`,{pollMs:3e4}),[o,u]=g.useState(new Set),[p,m]=g.useState(null),x=(l==null?void 0:l.username)||"this account",f=(l==null?void 0:l.devices)??[],h=(l==null?void 0:l.revisions)??[],j=(l==null?void 0:l.catalogue)??[],y=b=>u(T=>{const k=new Set(T);return k.has(b)?k.delete(b):k.add(b),k}),v=b=>a("restore",async()=>{await n(()=>D.post(`${i}/preferences/revisions/${b}/restore`),`Restored r${b}.`),m(null),await c()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Settings history",intro:`Every change to ${x}'s synced settings, and which of their televisions has taken it.`,crumbs:e.jsxs(Z,{to:`/admin/accounts/${encodeURIComponent(s)}`,children:["← ",x]})}),e.jsx(_,{message:r}),d?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Where each device has got to",intro:"A set takes a change by fetching it, which it does within a few seconds of being told — so anything still behind is switched off, mid-film, or cannot reach the gateway.",icon:"tv",tone:"info",actions:l!=null&&l.saved?e.jsxs(N,{tone:l.currentSource==="admin"?"warn":"ok",children:["now on r",w(l.currentRevision)," · ",l.currentSource||"device"]}):e.jsx(N,{children:"defaults · never synced"}),children:f.length===0?e.jsx(ee,{children:"No television has been signed in to this account."}):e.jsx("div",{className:"list",children:f.map(b=>{const T=b.never?void 0:b.behind?"bad":"ok";return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[e.jsx("span",{className:"dot-state","data-tone":T})," ",b.name||"Memby TV"," ",b.signedOut?e.jsx(N,{children:"signed out"}):null]}),e.jsxs("p",{children:[b.never?"Has not fetched these settings yet":`Holding r${w(b.revision)} · taken ${F(b.ackedAt)}`,b.clientVersion?` · Memby ${b.clientVersion}`:"",b.signedOut?"":` · last seen ${F(b.lastSeen)}`]})]}),e.jsx("div",{className:"list-actions",children:b.never?e.jsx(N,{children:"never taken one"}):b.behind?e.jsxs(N,{tone:"bad",children:[w(b.behind)," behind"]}):e.jsx(N,{tone:"ok",children:"up to date"})})]},b.deviceId||b.name)})})}),e.jsx(M,{title:"Change history",intro:"Restoring puts an earlier version back as a new change, so the televisions notice it and the version it replaced stays here to return to.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{className:"num",children:"Rev"}),e.jsx("th",{children:"Changed by"}),e.jsx("th",{children:"What changed"}),e.jsx("th",{className:"num",children:"Taken by"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:h.length===0?e.jsx(Y,{columns:6,children:"Nothing has been changed on this account yet."}):h.flatMap(b=>{const T=o.has(b.revision),k=b.acks??[],I=b.changes??[],E=[e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(b.createdAt)}),e.jsxs("td",{className:"num nowrap",children:["r",w(b.revision)," ",b.current?e.jsx(N,{tone:"ok",children:"current"}):null]}),e.jsxs("td",{className:"nowrap",children:[e.jsx(N,{tone:b.source==="admin"?"warn":"ok",children:b.author}),b.restoredFrom?e.jsxs("span",{className:"muted",children:[" restored r",w(b.restoredFrom)]}):null]}),e.jsx("td",{className:"muted",children:b.initial?e.jsx("span",{className:"muted",children:"First recorded settings"}):I.length===0?e.jsx("span",{className:"muted",children:"No visible change"}):e.jsx("div",{className:"chips",children:I.map((A,L)=>e.jsxs(ce,{children:[A.name,": ",A.before," → ",A.after]},`${A.name}:${L}`))})}),e.jsx("td",{className:"num",children:k.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{title:k.map(A=>A.deviceName||A.deviceId).join(", "),children:w(k.length)})}),e.jsx("td",{className:"num nowrap",children:e.jsxs("span",{className:"list-actions",children:[e.jsx($,{size:"sm",onClick:()=>y(b.revision),children:T?"Hide":"Show"}),b.current?null:e.jsx($,{size:"sm",onClick:()=>m(b.revision),children:"Restore"})]})})]},b.revision)];return T&&E.push(e.jsx("tr",{children:e.jsx("td",{colSpan:6,className:"muted",children:e.jsx("div",{className:"chips",children:j.map(A=>{var L;return e.jsxs(ce,{children:[A.name,":"," ",kt(A,(L=b.preferences)==null?void 0:L[A.key])]},A.key)})})})},`${b.revision}:detail`)),E})})]})})})]}),p!==null?e.jsx(ke,{title:`Restore revision ${p}?`,body:"It goes out as a new change, so every one of their televisions will pick it up — and the current version stays in this history to return to.",confirmLabel:"Restore",busy:t==="restore",onConfirm:()=>void v(p),onCancel:()=>m(null)}):null]})}function St({client:s}){const n=s.versions??[];return n.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{className:"versions",children:n.map(t=>e.jsx(ce,{tone:t.version===s.version?"ok":void 0,children:t.version},t.version))})}function Ct(){const{status:s,error:n,loading:t}=je(),a=(s==null?void 0:s.clients)??[],i=a.filter(r=>(r.capabilities??[]).includes("server_features_v1")),l=new Set(a.map(r=>r.version).filter(Boolean));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands."}),e.jsx(_,{message:n}),t?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"devices known",value:w(a.length),icon:"tv",tone:"info"},{label:"active in the last quarter hour",value:w(a.filter(r=>Ye(r.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"reporting their capabilities",value:w(i.length),icon:"sliders",tone:"ok"},{label:"app builds in service",value:w(l.size),icon:"download",tone:"note"}]}),e.jsx(M,{title:"Devices",intro:"Every request carries what that build understands. A feature is only presented to a device that declares its contract, which is what lets an older set keep working while a new one gets the new behaviour. Status is whether the set is reporting that list at all; a build old enough to say nothing is served the fallback.",icon:"tv",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Device"}),e.jsx("th",{children:"Person"}),e.jsx("th",{children:"App"}),e.jsx("th",{children:"Builds seen"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Last seen"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(Y,{columns:6,children:"No devices have signed in yet."}):a.map(r=>{const d=(r.capabilities??[]).includes("server_features_v1"),c=Ns(r.lastSeen);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":c.tone,title:c.label}),e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||"Memby TV"})]})}),e.jsx("td",{className:"muted",children:r.username}),e.jsx("td",{className:"mono",children:r.version||"legacy"}),e.jsx("td",{children:e.jsx(St,{client:r})}),e.jsx("td",{children:e.jsx(N,{tone:d?"ok":"warn",children:d?"reported":"missing"})}),e.jsx("td",{className:"nowrap muted",children:F(r.lastSeen)})]},`${r.deviceId}:${r.username}`)})})]})})})]})]})}const Ws={user:"",q:"",ip:"",outcome:"",from:"",to:""},Mt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function $t(){var b,T,k,I,E,A;const[s,n]=g.useState("log"),[t,a]=g.useState(7),[i,l]=g.useState(Ws),[r,d]=g.useState(0),c=100,o=g.useMemo(()=>Ve({...i,days:i.from?void 0:t||void 0,limit:c,offset:r*c}),[i,t,r]),u=J(`/admin/api/logins${o}`,{enabled:s==="log"}),p=J(`/admin/api/logins/devices${o}`,{enabled:s==="devices"}),m=((b=u.data)==null?void 0:b.users)??((T=p.data)==null?void 0:T.users)??[],x=((k=u.data)==null?void 0:k.totals)??((I=p.data)==null?void 0:I.totals),f=((E=u.data)==null?void 0:E.retentionDays)??((A=p.data)==null?void 0:A.retentionDays)??90,h=s==="log"?u.loading:p.loading,j=s==="log"?u.error:p.error,y=L=>{l(X=>({...X,...L})),d(0)},v=Object.entries(i).some(([,L])=>L!=="")||!!i.from;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Sign-in history",intro:"Every connection attempt, kept as history rather than as the latest state of a device. A television that has since been removed still appears here, because it still connected.",actions:e.jsx(Ue,{value:s,options:[{value:"log",label:"Log"},{value:"devices",label:"By device"}],onChange:n})}),e.jsx(_,{message:j}),x?e.jsx(ie,{tiles:[{label:"Successful sign-ins",value:w(x.logins),icon:"key",tone:"ok"},{label:"Refused",value:w(x.failures),icon:"shield",tone:x.failures>0?"warn":void 0},{label:"Televisions",value:w(x.devices),icon:"tv",tone:"info"},{label:"People",value:w(x.users),icon:"people",tone:"note"},{label:"Addresses",value:w(x.addresses),icon:"globe",tone:"data"},{label:"History kept",value:`${f} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(q,{label:"Window",children:e.jsx(Ue,{value:i.from?-1:t,options:Mt.map(L=>({value:L.value,label:L.label})),onChange:L=>{a(L),y({from:"",to:""})}})}),e.jsx(q,{label:"Person",children:e.jsxs("select",{value:i.user,onChange:L=>y({user:L.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),m.map(L=>e.jsx("option",{value:L.id,children:L.username||L.id},L.id))]})}),e.jsx(q,{label:"Outcome",children:e.jsxs("select",{value:i.outcome,onChange:L=>y({outcome:L.target.value}),children:[e.jsx("option",{value:"",children:"Both"}),e.jsx("option",{value:"success",children:"Got in"}),e.jsx("option",{value:"failure",children:"Refused"})]})}),e.jsx(q,{label:"Address",children:e.jsx("input",{type:"text",value:i.ip,placeholder:"10.0.0.4",onChange:L=>y({ip:L.target.value})})}),e.jsx(q,{label:"From",children:e.jsx("input",{type:"date",value:i.from,onChange:L=>y({from:L.target.value})})}),e.jsx(q,{label:"To",children:e.jsx("input",{type:"date",value:i.to,onChange:L=>y({to:L.target.value})})}),e.jsx(q,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:i.q,placeholder:"Name, device or address",onChange:L=>y({q:L.target.value})})}),e.jsx("div",{className:"filter-actions",children:v?e.jsx($,{variant:"quiet",size:"sm",onClick:()=>{l(Ws),d(0)},children:"Clear"}):null})]}),h?e.jsx(V,{}):s==="log"?e.jsx(Rt,{data:u.data,page:r,limit:c,onPage:d}):e.jsx(Tt,{data:p.data})]})}function Rt({data:s,page:n,limit:t,onPage:a}){if(!s)return null;const i=s.events.length,l=s.total===0?0:n*t+1;return e.jsxs(e.Fragment,{children:[e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Attempts per day",intro:"Grouped in the server's own timezone, so an evening sign-in stays on the day it happened.",icon:"chart",tone:"info",children:e.jsx(hs,{data:s.days,labelOf:r=>r.day,valueOf:r=>r.logins+r.failures,toneOf:r=>r.failures>r.logins?"bad":void 0,title:r=>`${r.day}: ${r.logins} in, ${r.failures} refused, ${r.devices} televisions`})}),e.jsx(M,{title:"Where from",icon:"globe",tone:"data",children:s.addresses.length===0?e.jsx(ee,{children:"No addresses in this window."}):e.jsx("div",{className:"list",children:s.addresses.slice(0,8).map(r=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:r.ipAddress}),e.jsxs("p",{children:[w(r.logins)," in",r.failures>0?` · ${w(r.failures)} refused`:""]})]}),r.failures>0&&r.logins===0?e.jsx(N,{tone:"bad",children:"only refused"}):null]},r.ipAddress))})})]}),e.jsx(M,{title:"Attempts",intro:"Uncollapsed and newest first: this is what to read when somebody says a television will not sign in.",icon:"key",tone:"ok",actions:e.jsx("span",{className:"filter-summary",children:s.total===0?"nothing matches":`${w(l)}–${w(l+i-1)} of ${w(s.total)}`}),footer:s.total>t?e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",disabled:n===0,onClick:()=>a(n-1),children:"Newer"}),e.jsx($,{size:"sm",disabled:(n+1)*t>=s.total,onClick:()=>a(n+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Person"}),e.jsx("th",{children:"Television"}),e.jsx("th",{className:"nowrap",children:"Address"}),e.jsx("th",{children:"Build"}),e.jsx("th",{children:"Outcome"})]})}),e.jsx("tbody",{children:s.events.length===0?e.jsx(Y,{columns:6,children:"No sign-in attempts match these filters."}):s.events.map(r=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(r.occurredAt)}),e.jsx("td",{children:r.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{children:r.deviceId?e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||r.deviceId}):e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"mono nowrap",children:r.ipAddress||"—"}),e.jsx("td",{className:"mono",children:r.clientVersion||"—"}),e.jsx("td",{className:"nowrap",children:r.success?r.newDevice?e.jsx(N,{tone:"info",children:"first sign-in"}):e.jsx(N,{tone:"ok",children:"got in"}):e.jsx(N,{tone:"bad",children:r.failureReason||"refused"})})]},r.id))})]})})})]})}function Tt({data:s}){return s?e.jsx(M,{title:"Televisions",intro:"Grouped from the history, not from the session list — a set whose session has expired still connected, and this is the record of it.",icon:"tv",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Television"}),e.jsx("th",{children:"Person"}),e.jsx("th",{className:"num",children:"Today"}),e.jsx("th",{className:"num",children:"Sign-ins"}),e.jsx("th",{className:"num",children:"Refused"}),e.jsx("th",{className:"num",children:"Addresses"}),e.jsx("th",{className:"nowrap",children:"Last address"}),e.jsx("th",{className:"nowrap",children:"Last sign-in"}),e.jsx("th",{children:"Build"})]})}),e.jsx("tbody",{children:s.devices.length===0?e.jsx(Y,{columns:9,children:"No television has connected in this window."}):s.devices.map(n=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(n.deviceId)}`,children:n.deviceName||n.deviceId})}),e.jsx("td",{className:"muted",children:n.username||"—"}),e.jsx("td",{className:"num",children:n.loginsToday>0?w(n.loginsToday):"—"}),e.jsx("td",{className:"num",children:w(n.logins)}),e.jsx("td",{className:"num",children:n.failures>0?e.jsx("span",{className:"mono",children:w(n.failures)}):"—"}),e.jsx("td",{className:"num",children:w(n.distinctIps)}),e.jsx("td",{className:"mono nowrap",children:n.lastIp||"—"}),e.jsx("td",{className:"nowrap muted",children:n.lastLogin?F(n.lastLogin):"—"}),e.jsx("td",{className:"mono",children:n.clientVersion||"—"})]},n.deviceId))})]})})}):null}const At=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Et(){const{deviceId:s=""}=Be(),[n,t]=g.useState(7),a=g.useMemo(()=>`/admin/api/logins/devices/${encodeURIComponent(s)}${Ve({days:n||void 0,limit:200})}`,[s,n]),{data:i,error:l,loading:r}=J(a,{enabled:!!s}),d=i==null?void 0:i.summary,c=(d==null?void 0:d.deviceName)||s;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:c,intro:"One television's whole relationship with the gateway.",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(Z,{to:"/admin/clients",children:"Devices"}),e.jsx("span",{children:"/"}),e.jsx(Z,{to:"/admin/logins",children:"Sign-ins"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:c})]}),actions:e.jsx(Ue,{value:n,options:At.map(o=>({value:o.value,label:o.label})),onChange:t})}),e.jsx(_,{message:l}),r?e.jsx(V,{}):i?e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"Sign-ins today",value:w((d==null?void 0:d.loginsToday)??0),icon:"clock",tone:"ok"},{label:"Sign-ins in total",value:w((d==null?void 0:d.logins)??0),icon:"key",tone:"info"},{label:"Refused",value:w((d==null?void 0:d.failures)??0),icon:"shield",tone:((d==null?void 0:d.failures)??0)>0?"warn":void 0},{label:"Addresses seen",value:w((d==null?void 0:d.distinctIps)??0),icon:"globe",tone:"data"},{label:"First seen",value:d!=null&&d.firstLogin?F(d.firstLogin):"—",small:!0,icon:"history"},{label:"Last seen",value:d!=null&&d.lastLogin?F(d.lastLogin):"—",small:!0,icon:"pulse",tone:"note"}]}),e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Connections per day",icon:"chart",tone:"info",children:e.jsx(hs,{data:i.days,labelOf:o=>o.day,valueOf:o=>o.logins+o.failures,toneOf:o=>o.failures>o.logins?"bad":void 0,title:o=>`${o.day}: ${o.logins} in${o.failures?`, ${o.failures} refused`:""}`})}),e.jsxs("div",{className:"stack",children:[e.jsx(M,{title:"Identity",icon:"tv",tone:"info",children:e.jsxs("div",{className:"list",children:[e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Person"}),e.jsx("p",{children:(d==null?void 0:d.username)||"unknown"})]})}),e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Device id"}),e.jsx("p",{className:"mono",children:i.deviceId})]})}),e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Running"}),e.jsx("p",{className:"mono",children:(d==null?void 0:d.clientVersion)||"unknown"})]})})]})}),e.jsx(M,{title:"Builds",intro:"Kept per television rather than per session, so it survives a sign-out.",icon:"upload",tone:"note",children:i.versions.length===0?e.jsx(ee,{children:"No build history for this television."}):e.jsx("div",{className:"list",children:i.versions.map(o=>e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:o.version}),e.jsxs("p",{children:[F(o.firstSeen)," → ",F(o.lastSeen)]})]})},o.version))})})]})]}),e.jsx(M,{title:"Addresses",icon:"globe",tone:"data",children:i.addresses.length===0?e.jsx(ee,{children:"No addresses recorded in this window."}):e.jsx("div",{className:"chips",children:i.addresses.map(o=>e.jsxs(ce,{tone:o.failures>0?"warn":"data",children:[o.ipAddress," · ",w(o.logins),o.failures>0?` (+${w(o.failures)} refused)`:""]},o.ipAddress))})}),e.jsx(M,{title:"Every attempt",icon:"key",tone:"ok",actions:e.jsxs("span",{className:"filter-summary",children:[w(i.total)," in this window"]}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Person"}),e.jsx("th",{className:"nowrap",children:"Address"}),e.jsx("th",{children:"Build"}),e.jsx("th",{children:"Method"}),e.jsx("th",{children:"Outcome"})]})}),e.jsx("tbody",{children:i.events.length===0?e.jsx(Y,{columns:6,children:"This television has not connected in the selected window."}):i.events.map(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(o.occurredAt)}),e.jsx("td",{children:o.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{className:"mono nowrap",children:o.ipAddress||"—"}),e.jsx("td",{className:"mono",children:o.clientVersion||"—"}),e.jsx("td",{className:"muted",children:o.method}),e.jsx("td",{className:"nowrap",children:o.success?o.newDevice?e.jsx(N,{tone:"info",children:"first sign-in"}):e.jsx(N,{tone:"ok",children:"got in"}):e.jsx(N,{tone:"bad",children:o.failureReason||"refused"})})]},o.id))})]})})})]}):null]})}function It(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(!1),o=(s==null?void 0:s.library.byType)??{},u=!!(s!=null&&s.syncRunning),p=m=>r(m,async()=>{await i(()=>D.post("/admin/api/sync",{kind:m}),m==="full"?"Full re-import started.":"Import started."),c(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Library",intro:"Import and inspect the catalogue Memby ranks."}),e.jsx(_,{message:n}),t||!s?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"items",value:w(s.library.total),icon:"library",tone:"data"},...Object.keys(o).sort().map(m=>({label:m,value:w(o[m]),icon:"list"})),{label:"last import",value:F(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsx(M,{title:"Import the catalogue",intro:"Emby's catalogue is copied here so search and the recommendation candidate pool can be answered from one indexed table. Watched, favourite and resume state is deliberately not stored — that is per person and still comes from Emby live.",icon:"library",tone:"data",footer:e.jsx("span",{className:"hint",children:u?"Import running…":`An incremental import runs automatically every ${s.syncEvery}.`}),children:e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",icon:"sync",disabled:u,busy:l==="incremental",onClick:()=>void p("incremental"),children:"Sync new items"}),e.jsx($,{icon:"database",disabled:u,onClick:()=>c(!0),children:"Full re-import"})]})})]}),d?e.jsx(ke,{title:"Re-import the entire library?",body:"A full pass mark-and-sweeps the catalogue and can take several minutes on a large library. Televisions keep reading the current table throughout.",confirmLabel:"Re-import",busy:l==="full",onConfirm:()=>void p("full"),onCancel:()=>c(!1)}):null]})}function Lt(){var p;const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),d=(s==null?void 0:s.requestUsers)??[],c=((p=s==null?void 0:s.requestPolicy)==null?void 0:p.allowedUserIds)??[],o=g.useMemo(()=>new Map(((s==null?void 0:s.requestUsage)??[]).map(m=>[m.userId,m])),[s==null?void 0:s.requestUsage]),u=m=>r(`access-${m}`,async()=>{const x=c.includes(m)?c.filter(f=>f!==m):[...c,m];await i(()=>D.post("/admin/api/request-policy",{allowedUserIds:x}),x.includes(m)?"Request access granted.":"Request access removed."),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Media requests",intro:"Who can ask for something the library does not have."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Where a request goes",intro:"Movies follow Radarr’s policy. Series follow the dedicated Sonarr request policy: monitored as normal, with backlog searching only when an operator enables it under Integrations.",icon:"inbox",tone:"info",actions:e.jsxs(e.Fragment,{children:[e.jsxs(N,{tone:s!=null&&s.radarrReady?"ok":"bad",children:["Movies ",s!=null&&s.radarrReady?"ready":"not configured"]}),e.jsxs(N,{tone:s!=null&&s.sonarrReady?"ok":"bad",children:["Series ",s!=null&&s.sonarrReady?"ready":"not configured"]})]}),children:!(s!=null&&s.radarrReady)&&!(s!=null&&s.sonarrReady)?e.jsx(ee,{children:"Neither Radarr nor Sonarr is configured, so a request would have nowhere to go. The button stays hidden on every television until one of them is."}):null}),e.jsx(M,{title:"Request access and activity",intro:"One button grants or removes access. A recorded request has already been sent to Radarr or Sonarr; Memby does not duplicate their download state.",icon:"people",tone:"note",children:d.length===0?e.jsx(ee,{children:"No one has signed in yet."}):e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"User"}),e.jsx("th",{children:"Last seen"}),e.jsx("th",{className:"num",children:"Sent to services"}),e.jsx("th",{children:"Last request"}),e.jsx("th",{children:"Access"})]})}),e.jsx("tbody",{children:d.map(m=>{const x=o.get(m.id),f=c.includes(m.id);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("b",{children:m.username})}),e.jsx("td",{className:"muted nowrap",children:F(m.lastSeen)}),e.jsx("td",{className:"num",children:(x==null?void 0:x.requests)??0}),e.jsx("td",{className:"muted nowrap",children:x!=null&&x.lastRequest?F(x.lastRequest):"—"}),e.jsx("td",{children:e.jsx($,{size:"sm",variant:f?"quiet":"primary",busy:l===`access-${m.id}`,onClick:()=>void u(m.id),children:f?"Remove access":"Give access"})})]},m.id)})})]})})})]})]})}function qt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(!1),o=s==null?void 0:s.forYou,u=!!(s!=null&&s.forYouRunning),p=(m,x,f)=>r(x,async()=>{await i(()=>D.post("/admin/api/for-you",{action:m}),f),c(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"For You",intro:"The prepared pools personalised rows are drawn from."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"Tracearr sessions",value:w((o==null?void 0:o.tracearrSessions)??0),icon:"play",tone:"info"},{label:"user profiles",value:w((o==null?void 0:o.profiles)??0),icon:"people",tone:"note"},{label:"ranked candidates",value:w((o==null?void 0:o.candidates)??0),icon:"sparkle",tone:"note"},{label:"last full import",value:F(o==null?void 0:o.lastFullImport),small:!0,icon:"clock"}]}),e.jsx(M,{title:"Pool maintenance",intro:"Prepared pools refresh in the background; these are the manual versions of the same work. A rebuild is safe at any time — televisions read the last finished pool until a new one lands.",icon:"sparkle",tone:"note",footer:e.jsx("span",{className:"hint",children:u?"For You maintenance running…":"Prepared pools normally refresh in the background."}),children:e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",icon:"download",disabled:u,busy:l==="import",onClick:()=>void p("incremental-import","import","Import started."),children:"Import recent sessions"}),e.jsx($,{icon:"database",disabled:u,onClick:()=>c(!0),children:"Full Tracearr backfill"}),e.jsx($,{icon:"sync",disabled:u,busy:l==="rebuild",onClick:()=>void p("rebuild-all","rebuild","Rebuild started."),children:"Rebuild all pools"})]})}),e.jsx(M,{title:"Reading a person's scores",intro:"The inspector re-runs the shared weighted scorer over one person's prepared pool, after Emby permission and parental-control filtering, and shows every component and evidence reason behind the order.",icon:"search",tone:"info",actions:e.jsx(Z,{to:"/admin/inspector",children:"Open the inspector"}),children:e.jsx(e.Fragment,{})})]}),d?e.jsx(ke,{title:"Backfill all Tracearr history?",body:"Every session is re-read and every active user's pool is rebuilt. It is safe at any time — televisions keep reading the last finished pool — but on a long history it takes a while.",confirmLabel:"Backfill",busy:l==="full",onConfirm:()=>void p("full-import","full","Backfill started."),onCancel:()=>c(!1)}):null]})}const Dt=[["Genre","genres"],["Studio","studios"],["Actor","actors"],["Director","directors"],["Franchise","franchises"],["Runtime","runtimeRanges"],["Age rating","ageRatings"],["Community rating","communityRatings"],["Release period","releasePeriods"],["Content type","contentTypes"]];function Ft(s){return s?Dt.flatMap(([n,t])=>Object.entries(s[t]??{}).map(([a,i])=>({dimension:n,name:a,weight:i.weight??0,evidence:i.evidence??0}))).sort((n,t)=>Math.abs(t.weight)-Math.abs(n.weight)):[]}const Bs=s=>`${s>=0?"+":""}${s.toFixed(3)}`;function Ot(){const{status:s}=je(),{wrap:n}=ae(),{busy:t,run:a}=ne(),[i,l]=g.useState(""),[r,d]=g.useState("default"),[c,o]=g.useState("0"),[u,p]=g.useState(""),[m,x]=g.useState(null),[f,h]=g.useState("Choose a person to inspect their recommendations."),[j,y]=g.useState(""),v=(s==null?void 0:s.requestUsers)??[],b=()=>a("run",async()=>{if(!i){y("Choose a person to pressure-test.");return}y(""),h("Running the permission check and the scorer…");const A=new URLSearchParams({userId:i,context:r,minutes:c||"0",limit:"100"});u&&A.set("at",new Date(u).toISOString());const L=await n(()=>D.get(`/admin/api/recommendations?${A.toString()}`));L?(x(L),h(`Scored at ${new Date().toLocaleTimeString()}.`)):h("Pressure test failed.")}),T=Ft((m==null?void 0:m.profile)??null).slice(0,24),k=(m==null?void 0:m.actions)??[],I=(m==null?void 0:m.items)??[],E=(m==null?void 0:m.profileMeta)??{};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Score inspector",intro:"Re-run the ranker for one person and read every component."}),e.jsx(_,{message:j}),e.jsx(M,{title:"Run a pressure test",intro:"Nothing is changed by running this. It scores the person's prepared pool as the launcher would, in the context you choose.",icon:"search",tone:"info",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:t==="run",onClick:()=>void b(),children:"Run pressure test"}),e.jsx("span",{className:"hint",children:f})]}),children:e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Person",children:e.jsxs("select",{value:i,onChange:A=>l(A.target.value),children:[e.jsx("option",{value:"",children:"Choose a person…"}),v.map(A=>e.jsx("option",{value:A.id,children:A.username},A.id))]})}),e.jsx(q,{label:"Context",children:e.jsxs("select",{value:r,onChange:A=>d(A.target.value),children:[e.jsx("option",{value:"default",children:"Default"}),e.jsx("option",{value:"bedtime",children:"One episode before bed"}),e.jsx("option",{value:"hidden",children:"Hidden library"}),e.jsx("option",{value:"new-releases",children:"Recent new releases"})]})}),e.jsx(q,{label:"Available minutes",children:e.jsx("input",{type:"number",min:0,max:360,value:c,onChange:A=>o(A.target.value)})}),e.jsx(q,{label:"Evaluate at",children:e.jsx("input",{type:"datetime-local",value:u,onChange:A=>p(A.target.value)})})]})}),m?e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"prepared pool",value:w(m.poolCandidates),icon:"database",tone:"data"},{label:"permission eligible",value:w(m.permissionEligible),icon:"shield",tone:"ok"},{label:"ranked result",value:w(I.length),icon:"sparkle",tone:"note"},{label:"source events",value:w(E.sourceEvents??0),icon:"pulse",tone:"info"},{label:"algorithm",value:E.algorithmVersion||"—",small:!0,icon:"chip"},{label:"pool built",value:F(E.poolBuiltAt),small:!0,icon:"clock"}]}),e.jsxs(M,{title:"Profile evidence",intro:"The strongest learned affinities, and every explicit action this person has taken.",icon:"sparkle",tone:"note",children:[T.length===0?e.jsx(ee,{children:"No repeated affinity evidence yet; cold-start priors apply."}):e.jsx("div",{className:"chips",children:T.map(A=>e.jsxs(ce,{tone:A.weight<0?"bad":void 0,children:[A.dimension,": ",A.name," ",Bs(A.weight)," · n=",w(A.evidence)]},`${A.dimension}:${A.name}`))}),k.length===0?e.jsx(ee,{children:"No explicit recommendation actions."}):e.jsx("div",{className:"chips",children:k.map((A,L)=>e.jsxs(ce,{tone:"ok",children:[A.action,": ",A.title||A.itemId]},`${A.action}:${L}`))})]}),I.length===0?e.jsx(M,{children:e.jsx(ee,{children:"No candidates survived this context, the explicit exclusions and the permission filter."})}):e.jsx(xe,{children:I.map((A,L)=>{const X=A.explanation??{},le=Object.entries(X.components??{}).sort((H,re)=>Math.abs(re[1])-Math.abs(H[1])),ue=A.exposure??{},te=[A.type,A.year,A.runtimeMinutes?`${A.runtimeMinutes} min`:null,...A.genres??[]].filter(Boolean).join(" · ");return e.jsxs(M,{title:`#${L+1} · ${A.title}`,intro:te,actions:e.jsx(ce,{tone:"ok",children:Number(X.total??0).toFixed(3)}),children:[e.jsxs("p",{className:"hint",children:[A.preparedReason||"No legacy prepared explanation",A.compatibilityLabel?` · ${A.compatibilityLabel}`:""]}),e.jsxs("div",{className:"chips",children:[(X.reasonCodes??[]).map(H=>e.jsx(ce,{tone:"ok",children:H},H)),le.map(([H,re])=>e.jsxs(ce,{tone:re<0?"bad":void 0,children:[H,"=",Bs(re)]},H))]}),e.jsxs("details",{children:[e.jsx("summary",{className:"muted",children:"Pool, row and exposure detail"}),e.jsxs("p",{className:"hint",children:["Base rank ",w(A.baseRank??0)," · base ",Number(A.baseScore??0).toFixed(3)," · affinity ",Number(A.affinityScore??0).toFixed(3)," · compatibility"," ",Number(A.compatibilityScore??0).toFixed(3)," · impressions"," ",w(ue.impressions??0)," · focuses ",w(ue.focuses??0)," · selects"," ",w(ue.selects??0)]}),e.jsx("div",{className:"chips",children:(A.eligibleRows??[]).map(H=>e.jsx(ce,{tone:"ok",children:H},H))}),A.preparedEvidenceTitle?e.jsxs("p",{className:"hint",children:["Prepared evidence: ",A.preparedEvidenceTitle]}):null]})]},`${L}:${A.title}`)})})]}):null]})}const Pt=4,Ee=[{id:"home",label:"Home",type:"films and television shows"},{id:"movies",label:"Movies",type:"films"},{id:"tv_shows",label:"TV Shows",type:"television shows"}],ze=()=>({pinnedItems:[],primeSubtitle:""}),bs=[{value:1,short:"Mon",label:"Monday"},{value:2,short:"Tue",label:"Tuesday"},{value:3,short:"Wed",label:"Wednesday"},{value:4,short:"Thu",label:"Thursday"},{value:5,short:"Fri",label:"Friday"},{value:6,short:"Sat",label:"Saturday"},{value:0,short:"Sun",label:"Sunday"}];function Vs(s){const n=s.getTimezoneOffset()*6e4;return new Date(s.getTime()-n).toISOString().slice(0,16)}function Hs(s){return!!(s&&Number.isFinite(new Date(s).getTime())&&new Date(s).getFullYear()>=2e3)}function _t(s){return s.frequency??"once"}function Ut(s){if(s.frequency==="daily")return`Every day · ${s.startTime}–${s.endTime}`;if(s.frequency==="weekly"){const n=bs.filter(a=>(s.weekdays??[]).includes(a.value));return`${(n.length===7?"Every day":n.map(a=>a.short).join(", "))||"No days selected"} · ${s.startTime}–${s.endTime}`}return`${s.startAt?new Date(s.startAt).toLocaleString():"Start missing"} → ${s.endAt?new Date(s.endAt).toLocaleString():"End missing"}`}function zs(s,n){const t=new Date;t.setMinutes(Math.ceil(t.getMinutes()/30)*30,0,0);const a=new Date(t.getTime()+2*60*60*1e3),i=n==="home"||n==="movies"&&s.type==="Movie"||n==="tv_shows"&&s.type==="Series";return{id:crypto.randomUUID(),itemId:s.id,startAt:t.toISOString(),endAt:a.toISOString(),priority:0,enabled:!0,placements:[i?n:"home"]}}function Wt(){var te,H,re;const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i,show:l}=ae(),{busy:r,run:d}=ne(),[c,o]=g.useState("home"),[u,p]=g.useState({home:ze(),movies:ze(),tv_shows:ze()}),[m,x]=g.useState(!1),[f,h]=g.useState(""),[j,y]=g.useState(null),[v,b]=g.useState([]),[T,k]=g.useState(null),I=s==null?void 0:s.heroPolicy;g.useEffect(()=>{var S,O,z;m||!I||(p({home:((S=I.placements)==null?void 0:S.home)??{pinnedItems:I.pinnedItems??[],primeSubtitle:I.primeSubtitle??""},movies:((O=I.placements)==null?void 0:O.movies)??ze(),tv_shows:((z=I.placements)==null?void 0:z.tv_shows)??ze()}),b(I.schedules??[]))},[I,m]);const E=()=>d("search",async()=>{const S=f.trim();if(!S)return;const O=await i(()=>D.get(`/admin/api/hero/search?q=${encodeURIComponent(S)}`));O&&y(O.items??[])}),A=u[c],L=A.pinnedItems??[],X=S=>{p(O=>({...O,[c]:{...O[c],...S}})),x(!0)},le=S=>{if(!L.some(O=>O.id===S.id)){if(L.length>=Pt){l("Remove a pinned title before adding another.","bad");return}X({pinnedItems:[...L,S]})}},ue=()=>d("save",async()=>{await i(()=>D.post("/admin/api/hero-policy",{placements:Object.fromEntries(Object.entries(u).map(([S,O])=>[S,{pinnedItemIds:(O.pinnedItems??[]).map(z=>z.id),primeSubtitle:O.primeSubtitle.trim()}])),schedules:v}),"Hero saved."),x(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Featured content",intro:"Manage an independent, backend-resolved hero for Home, Movies and TV Shows."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(xe,{children:Ee.map(S=>{var Te;const O=u[S.id],z=v.filter(ge=>ge.enabled&&(ge.placements??["home"]).includes(S.id)).sort((ge,R)=>R.priority-ge.priority)[0],oe=(O.pinnedItems??[]).length?"Manual":z?"Schedule ready":"Automatic",we=(Te=I==null?void 0:I.items)==null?void 0:Te.find(ge=>ge.id===(z==null?void 0:z.itemId)),Re=(O.pinnedItems??[]).map(ge=>ge.name).join(", ")||(we==null?void 0:we.name)||(z==null?void 0:z.itemId)||"Resolved for each viewer";return e.jsx(M,{title:S.label,intro:`${oe} · ${Re}`,tone:S.id===c?"info":void 0,children:e.jsxs($,{size:"sm",variant:"quiet",onClick:()=>o(S.id),children:["Manage ",S.label]})},S.id)})}),e.jsx("div",{className:"tabs",role:"tablist","aria-label":"Hero placement",children:Ee.map(S=>e.jsx($,{variant:c===S.id?"primary":"quiet",onClick:()=>o(S.id),children:S.label},S.id))}),e.jsxs(M,{title:`${(te=Ee.find(S=>S.id===c))==null?void 0:te.label} hero`,intro:`Pinned ${(H=Ee.find(S=>S.id===c))==null?void 0:H.type} lead this section only. Empty places use this placement’s automatic selection.`,icon:"star",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:r==="save",onClick:()=>void ue(),children:"Save hero"}),e.jsx($,{onClick:()=>{X({pinnedItems:[]})},children:"Clear pins"}),m?e.jsx("span",{className:"hint",children:"Unsaved changes."}):null]}),children:[L.length===0?e.jsx(ee,{children:"No titles are pinned. The hero is entirely release-aware and automatic."}):e.jsx("div",{className:"hero-pins",children:L.map((S,O)=>e.jsxs("div",{className:"hero-pin",children:[e.jsx("span",{className:"hero-pin-order",children:O+1}),e.jsxs("span",{children:[e.jsx("b",{children:S.name}),e.jsxs("small",{children:[S.type,S.year?` · ${S.year}`:""]})]}),e.jsx($,{size:"sm",icon:"clock",onClick:()=>k(zs(S,c)),children:"Schedule"}),e.jsx($,{variant:"quiet",size:"sm",icon:"close",title:`Remove ${S.name}`,onClick:()=>X({pinnedItems:L.filter(z=>z.id!==S.id)})})]},S.id))}),e.jsx(q,{label:"Prime-card subtitle",hint:"Optional wording under the large first card. Leave blank to use Memby's natural release or rating reason.",children:e.jsx("input",{type:"text",maxLength:160,value:A.primeSubtitle,placeholder:"Leave blank for the automatic reason",onChange:S=>{X({primeSubtitle:S.target.value})}})})]}),e.jsx(M,{title:"Hero schedule",intro:"The gateway applies these rules in server time. Manual pins win first; otherwise the highest-priority active schedule wins, followed by Memby’s automatic hero.",icon:"clock",tone:"info",actions:e.jsx(N,{tone:"info",children:(I==null?void 0:I.timeZone)||"server local time"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:r==="save",onClick:()=>void ue(),children:"Save schedule"}),e.jsx("span",{className:"hint",children:"Daily and weekly rules repeat until you switch them off."})]}),children:v.length===0?e.jsx(ee,{children:"No scheduled heroes yet. Use Schedule beside a pinned or searched title."}):e.jsx("div",{className:"hero-schedule-list",children:[...v].sort((S,O)=>Number(O.enabled)-Number(S.enabled)||O.priority-S.priority).map(S=>{const O=[...(I==null?void 0:I.items)??[],...L,...j??[]].find(z=>z.id===S.itemId);return e.jsxs("article",{className:"hero-schedule","data-enabled":S.enabled||void 0,children:[e.jsxs("div",{className:"hero-schedule-time",children:[e.jsx("b",{children:S.frequency==="weekly"?"Weekly":S.frequency==="daily"?"Daily":"Once"}),e.jsx("span",{children:S.frequency?S.startTime:S.startAt?new Date(S.startAt).toLocaleDateString():"—"})]}),e.jsxs("div",{className:"hero-schedule-main",children:[e.jsxs("div",{className:"hero-schedule-title",children:[e.jsx("h3",{children:(O==null?void 0:O.name)??S.itemId}),e.jsx(N,{tone:S.enabled?"ok":void 0,children:S.enabled?"enabled":"paused"})]}),e.jsx("p",{children:Ut(S)}),e.jsxs("div",{className:"chips",children:[(S.placements??["home"]).map(z=>{var oe;return e.jsx("span",{className:"chip",children:(oe=Ee.find(we=>we.id===z))==null?void 0:oe.label},z)}),S.priority!==0?e.jsxs("span",{className:"chip",children:["Priority ",S.priority]}):null]})]}),e.jsxs("div",{className:"hero-schedule-actions",children:[e.jsx($,{size:"sm",onClick:()=>k({...S}),children:"Edit"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{b(z=>z.map(oe=>oe.id===S.id?{...oe,enabled:!oe.enabled}:oe)),x(!0)},children:S.enabled?"Pause":"Enable"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{b(z=>z.filter(oe=>oe.id!==S.id)),x(!0)},children:"Remove"})]})]},S.id)})})}),e.jsxs(M,{title:"Find a title",intro:`Search the imported Emby catalogue. Add a result to the selected ${(re=Ee.find(S=>S.id===c))==null?void 0:re.label} placement; switch tabs to show it in more than one section.`,icon:"search",tone:"info",children:[e.jsxs("div",{className:"field-row",children:[e.jsx(q,{label:"Title",grow:!0,children:e.jsx("input",{type:"search",value:f,placeholder:"Search films and television shows",onChange:S=>h(S.target.value),onKeyDown:S=>{S.key==="Enter"&&E()}})}),e.jsx($,{busy:r==="search",icon:"search",onClick:()=>void E(),children:"Search"})]}),j===null?null:j.length===0?e.jsx(ee,{children:"No playable films or series matched that search."}):e.jsx(xe,{children:j.map(S=>e.jsx(M,{title:S.name,intro:`${S.type||"Title"} · ${S.year||"Year unknown"}`,children:e.jsxs("div",{className:"row",children:[e.jsx($,{size:"sm",icon:"plus",disabled:L.some(O=>O.id===S.id)||c==="movies"&&S.type!=="Movie"||c==="tv_shows"&&S.type!=="Series",onClick:()=>le(S),children:L.some(O=>O.id===S.id)?"Pinned":"Add to hero"}),e.jsx($,{size:"sm",icon:"clock",onClick:()=>k(zs(S,c)),children:"Schedule"})]})},S.id))})]})]}),T?e.jsx(Bt,{schedule:T,item:[...(I==null?void 0:I.items)??[],...L,...j??[]].find(S=>S.id===T.itemId),timeZone:(I==null?void 0:I.timeZone)||"server local time",isNew:!v.some(S=>S.id===T.id),onCancel:()=>k(null),onSave:S=>{b(O=>O.some(z=>z.id===S.id)?O.map(z=>z.id===S.id?S:z):[...O,S]),k(null),x(!0)}}):null]})}function Bt({schedule:s,item:n,timeZone:t,isNew:a,onSave:i,onCancel:l}){const[r,d]=g.useState({...s,weekdays:[...s.weekdays??[]]}),c=_t(r),o=h=>{const j=new Date,y=new Date(j.getTime()+2*60*60*1e3);d(v=>{var b;return h==="once"?{...v,frequency:void 0,startAt:Hs(v.startAt)?v.startAt:j.toISOString(),endAt:Hs(v.endAt)?v.endAt:y.toISOString()}:{...v,frequency:h,startTime:v.startTime||"18:00",endTime:v.endTime||"22:00",weekdays:h==="weekly"?(b=v.weekdays)!=null&&b.length?v.weekdays:[1,2,3,4,5]:[]}})},u=r.placements??["home"],p=h=>h==="home"||h==="movies"&&(n==null?void 0:n.type)==="Movie"||h==="tv_shows"&&(n==null?void 0:n.type)==="Series",m=!!(r.startAt&&r.endAt&&new Date(r.endAt)>new Date(r.startAt)),x=!!(r.startTime&&r.endTime&&r.startTime!==r.endTime&&(c!=="weekly"||(r.weekdays??[]).length>0)),f=c==="once"?m:x;return e.jsx("div",{className:"scrim",onPointerDown:h=>h.target===h.currentTarget&&l(),children:e.jsxs("div",{className:"dialog hero-schedule-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"hero-schedule-title",children:[e.jsxs("div",{className:"hero-schedule-dialog-head",children:[e.jsx("span",{className:"hero-schedule-kicker",children:"Hero schedule"}),e.jsx("h2",{id:"hero-schedule-title",children:(n==null?void 0:n.name)??r.itemId}),e.jsxs("p",{children:["Choose exactly when this title can lead the selected sections. Times use ",t,"."]})]}),e.jsx("div",{className:"schedule-frequency",role:"group","aria-label":"Schedule frequency",children:["once","daily","weekly"].map(h=>e.jsxs("button",{type:"button","aria-pressed":c===h,onClick:()=>o(h),children:[e.jsx("b",{children:h==="once"?"One time":h==="daily"?"Every day":"Weekly"}),e.jsx("span",{children:h==="once"?"A date range":h==="daily"?"Same time daily":"Choose days"})]},h))}),c==="once"?e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Starts",children:e.jsx("input",{type:"datetime-local",value:r.startAt?Vs(new Date(r.startAt)):"",onChange:h=>d(j=>({...j,startAt:h.target.value?new Date(h.target.value).toISOString():void 0}))})}),e.jsx(q,{label:"Ends",children:e.jsx("input",{type:"datetime-local",value:r.endAt?Vs(new Date(r.endAt)):"",onChange:h=>d(j=>({...j,endAt:h.target.value?new Date(h.target.value).toISOString():void 0}))})})]}):e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Starts each time",children:e.jsx("input",{type:"time",value:r.startTime??"",onChange:h=>d(j=>({...j,startTime:h.target.value}))})}),e.jsx(q,{label:"Ends each time",hint:"An earlier end time continues into the following day.",children:e.jsx("input",{type:"time",value:r.endTime??"",onChange:h=>d(j=>({...j,endTime:h.target.value}))})})]}),c==="weekly"?e.jsxs("div",{className:"schedule-days",children:[e.jsxs("div",{className:"schedule-days-head",children:[e.jsx("b",{children:"Days"}),e.jsxs("div",{children:[e.jsx("button",{type:"button",onClick:()=>d(h=>({...h,weekdays:[1,2,3,4,5]})),children:"Weekdays"}),e.jsx("button",{type:"button",onClick:()=>d(h=>({...h,weekdays:[6,0]})),children:"Weekend"}),e.jsx("button",{type:"button",onClick:()=>d(h=>({...h,weekdays:bs.map(j=>j.value)})),children:"Every day"})]})]}),e.jsx("div",{className:"schedule-day-grid",children:bs.map(h=>{const j=(r.weekdays??[]).includes(h.value);return e.jsx("button",{type:"button","aria-pressed":j,title:h.label,onClick:()=>d(y=>({...y,weekdays:j?(y.weekdays??[]).filter(v=>v!==h.value):[...y.weekdays??[],h.value]})),children:h.short},h.value)})})]}):null]}),e.jsxs("div",{className:"schedule-options",children:[e.jsxs("div",{children:[e.jsx("span",{className:"schedule-option-label",children:"Show in"}),e.jsx("div",{className:"schedule-placement-grid",children:Ee.map(h=>e.jsx(G,{label:h.label,checked:u.includes(h.id),disabled:!p(h.id),onChange:j=>d(y=>{const v=y.placements??["home"],b=j?[...v,h.id]:v.filter(T=>T!==h.id);return{...y,placements:b.length?[...new Set(b)]:v}})},h.id))})]}),e.jsx(q,{label:"Priority",hint:"Higher rules win when schedules overlap.",children:e.jsx("input",{type:"number",min:-1e3,max:1e3,step:10,value:r.priority,onChange:h=>d(j=>({...j,priority:Number(h.target.value)}))})})]}),e.jsx(G,{label:"Schedule enabled",hint:"Pause it without losing its days and times.",checked:r.enabled,onChange:h=>d(j=>({...j,enabled:h}))}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx($,{variant:"primary",disabled:!f,onClick:()=>i(r),children:a?"Add rule":"Save rule"})]})]})})}const Ks=["title","ratings","facts","summary"];function Vt(){var f;const s=J("/admin/api/metadata-hero"),{busy:n,run:t}=ne(),{wrap:a}=ae(),[i,l]=g.useState(null),[r,d]=g.useState(null);g.useEffect(()=>{i===null&&s.data&&l(s.data.settings.contentOrder),r===null&&s.data&&d(s.data.settings.timeRemainingColour??"green")},[i,s.data,r]);const c=i??Ks,o=r??"green",u=((f=s.data)==null?void 0:f.options)??[],p=[...c,...u.map(h=>h.value).filter(h=>!c.includes(h))],m=(h,j)=>{const y=c.indexOf(h),v=y+j;if(y<0||v<0||v>=c.length)return;const b=[...c],T=b[y];b[y]=b[v],b[v]=T,l(b)},x=()=>t("save",async()=>{const h=await a(()=>D.post("/admin/api/metadata-hero",{contentOrder:c,timeRemainingColour:o}),"Metadata hero saved for every television.");h&&(s.set(h),l(h.settings.contentOrder),d(h.settings.timeRemainingColour))});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Metadata hero",intro:"Choose which content appears beside focused browse cards, and the order every television uses."}),e.jsx(_,{message:s.error}),s.loading?e.jsx(V,{rows:2}):e.jsxs("div",{className:"metadata-hero-layout",children:[e.jsxs(M,{title:"Content order",intro:"Enabled blocks are drawn from top to bottom. Changes apply to the whole household.",icon:"sliders",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:n==="save",onClick:()=>void x(),children:"Save metadata hero"}),e.jsx($,{onClick:()=>{l(Ks),d("green")},children:"Restore defaults"})]}),children:[e.jsx("div",{className:"metadata-hero-order",children:p.map(h=>{const j=u.find(b=>b.value===h);if(!j)return null;const y=c.indexOf(h),v=y>=0;return e.jsxs("div",{className:"metadata-hero-order-row","data-enabled":v||void 0,children:[e.jsx("span",{className:"metadata-hero-order-number",children:v?y+1:"—"}),e.jsx(G,{label:j.label,hint:j.description,checked:v,disabled:v&&c.length===1,onChange:b=>l(b?[...c,h]:c.filter(T=>T!==h))}),e.jsxs("div",{className:"metadata-hero-order-actions",children:[e.jsx($,{size:"sm",variant:"quiet",disabled:!v||y===0,title:`Move ${j.label} up`,onClick:()=>m(h,-1),children:"↑"}),e.jsx($,{size:"sm",variant:"quiet",disabled:!v||y===c.length-1,title:`Move ${j.label} down`,onClick:()=>m(h,1),children:"↓"})]})]},h)})}),e.jsxs("div",{className:"metadata-hero-colour",children:[e.jsx("b",{children:"Time-remaining text colour"}),e.jsx("p",{children:"Choose the caption colour used beside the resume progress bar."}),e.jsx(Ue,{value:o,options:[{value:"green",label:"Emby green"},{value:"white",label:"White"}],onChange:d})]})]}),e.jsx(M,{title:"Television preview",intro:"A representative focused film at launcher scale and in the current order.",icon:"tv",children:e.jsxs("div",{className:"metadata-hero-preview","aria-label":"Metadata hero preview",children:[e.jsx("div",{className:"metadata-hero-preview-copy",children:c.map(h=>h==="title"?e.jsx("div",{className:"metadata-preview-title",children:"THE SOUTHERN LIGHT"},h):h==="ratings"?e.jsxs("div",{className:"metadata-preview-ratings",children:[e.jsx("b",{children:"IMDb 8.2"}),e.jsx("b",{children:"TMDb 81%"})]},h):h==="facts"?e.jsxs("div",{className:"metadata-preview-facts",children:[e.jsx("span",{children:"2026   •   1h 54m   •   M   •   Drama, Mystery"}),e.jsx("b",{children:"4K"}),e.jsx("b",{children:"5.1"})]},h):h==="recommendation_reason"?e.jsx("div",{className:"metadata-preview-reason",children:"Because you watched Harbour Lights"},h):h==="time_remaining"?e.jsxs("div",{className:"metadata-preview-progress","data-colour":o,children:[e.jsx("span",{children:e.jsx("i",{})}),e.jsx("b",{children:"47 minutes remaining"})]},h):h==="summary"?e.jsx("p",{className:"metadata-preview-summary",children:"A quiet coastal town follows an unexpected signal across the winter sky."},h):null)}),e.jsx("div",{className:"metadata-hero-preview-art","aria-hidden":"true"})]})})]})]})}function Ht(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(null),o=s==null?void 0:s.features,u=(o==null?void 0:o.features)??[],p=(s==null?void 0:s.clients)??[],m=(o==null?void 0:o.revision)??0,x=(y,v,b,T)=>r(v,async()=>{await i(()=>D.post("/admin/api/features",{action:y,expectedRevision:m,overrides:T??{}}),b),c(null),await a()}),f=p.filter(y=>(y.capabilities??[]).includes("server_features_v1")).length,h=!!(o!=null&&o.safeMode),j=(y,v)=>({...Object.fromEntries(u.filter(b=>b.source==="override").map(b=>[b.key,b.enabled])),[y]:v});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Features",intro:"Roll out, stop and recover optional behaviour with no app release."}),e.jsx(_,{message:n}),t||!o?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Control plane",intro:"Every optional feature has a safe default, an explicit override and a remote recovery path. Safe mode turns all of them off at once; sign-in, browsing and playback are never optional.",icon:"sliders",tone:"ok",actions:e.jsx($,{variant:h?void 0:"danger",busy:l==="safe",onClick:()=>h?void x("leave-safe-mode","safe","Safe mode ended."):c({action:"safe-mode",title:"Enable safe mode?",body:"Every optional feature is disabled immediately on every television. Core sign-in, browsing and playback remain available.",label:"Enable safe mode"}),children:h?"Leave safe mode":"Enable safe mode"}),children:e.jsx(Cs,{tiles:[{label:"features active",value:`${u.filter(y=>y.enabled).length} / ${u.length}`},{label:"explicit overrides",value:w(u.filter(y=>y.source==="override").length)},{label:"televisions reporting the control plane",value:`${f} / ${p.length}`},{label:"published revision",value:`r${w(m)}`}]})}),e.jsx(xe,{cols:"2",children:u.length===0?e.jsx(M,{title:"Nothing registered",icon:"sliders",children:e.jsx(ee,{children:"No server features are registered."})}):u.map(y=>e.jsxs(M,{title:y.name,intro:y.description,actions:e.jsx(N,{tone:y.enabled?"ok":void 0,children:y.enabled?"active":"off"}),footer:e.jsxs("span",{className:"hint",children:["↳ ",y.recovery]}),children:[e.jsx(G,{label:y.enabled?"On":"Off",hint:"Changing this applies the feature policy to every compatible television.",checked:y.enabled,disabled:l===y.key,onChange:v=>c({action:"feature",key:y.key,enabled:v,title:`${v?"Turn on":"Turn off"} ${y.name}?`,body:`${v?"Enable":"Disable"} this feature for every compatible television. ${y.recovery}`,label:v?"Turn on":"Turn off"})}),e.jsxs("div",{className:"chips",children:[e.jsx(ce,{children:y.key}),e.jsxs(ce,{children:["protocol ",w(y.minimumProtocol),"+"]}),e.jsx(ce,{tone:y.compatible?"ok":"warn",children:y.compatible?"server compatible":"compatibility blocked"}),e.jsx(ce,{tone:"note",children:y.area})]})]},y.key))}),e.jsx(M,{children:e.jsxs("div",{className:"row",children:[e.jsx($,{disabled:!o.canRollback,onClick:()=>c({action:"rollback",title:"Roll back one revision?",body:"The previous published feature revision is restored on every television.",label:"Roll back"}),children:"Roll back one revision"}),e.jsx($,{onClick:()=>c({action:"reset",title:"Clear every override?",body:"All features return to their safe software defaults.",label:"Clear overrides"}),children:"Clear all overrides"}),e.jsx("span",{className:"spacer"}),h?e.jsx(N,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(N,{tone:"ok",children:["live · revision r",w(m)]})]})})]}),d?e.jsx(ke,{title:d.title,body:d.body,confirmLabel:d.label,destructive:d.action!=="rollback",busy:l===d.action,onConfirm:()=>void x(d.action==="feature"?"save":d.action,d.action==="feature"?d.key??"feature":d.action,`${d.label} done.`,d.action==="feature"&&d.key?j(d.key,!!d.enabled):void 0),onCancel:()=>c(null)}):null]})}function zt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i,show:l}=ae(),{busy:r,run:d}=ne(),[c,o]=g.useState(!0),[u,p]=g.useState("6.5"),[m,x]=g.useState(!1);g.useEffect(()=>{var h,j;m||!s||(o(((h=s.playbackPolicy)==null?void 0:h.prerollEnabled)!==!1),p(String((((j=s.playbackPolicy)==null?void 0:j.prerollDurationMs)??6500)/1e3)))},[s,m]);const f=()=>d("save",async()=>{const h=Number(u);if(!Number.isFinite(h)||h<1||h>30){l("The preroll duration must be between 1 and 30 seconds.","bad");return}await i(()=>D.post("/admin/api/playback-policy",{prerollEnabled:c,prerollDurationMs:Math.round(h*1e3)}),"Playback policy saved."),x(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Playback",intro:"Presentation policy sent with every playback launch."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:1}):e.jsxs(M,{title:"Upcoming-show preroll",intro:"Sent with every playback launch. A change applies to the next title opened on every gateway-connected television; no app release is required.",icon:"play",tone:"info",actions:c?e.jsxs(N,{tone:"ok",children:["on · ",u,"s"]}):e.jsx(N,{children:"off"}),footer:e.jsx($,{variant:"primary",busy:r==="save",onClick:()=>void f(),children:"Save playback policy"}),children:[e.jsx(G,{label:"Show the preroll before a title starts",checked:c,onChange:h=>{o(h),x(!0)}}),e.jsx("div",{className:"fields",children:e.jsx(q,{label:"Duration",hint:"Between 1 and 30 seconds. The stream is already playing behind it.",children:e.jsx("input",{type:"number",min:1,max:30,step:.5,value:u,onChange:h=>{p(h.target.value),x(!0)}})})})]})]})}function Kt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(null),[o,u]=g.useState(null),[p,m]=g.useState(!1),x=s==null?void 0:s.subtitles,f=x==null?void 0:x.stored;g.useEffect(()=>{d||!x||c({bazarr:x.bazarrEnabled,openSubtitles:x.openSubtitlesEnabled,key:"",clearKey:!1,username:x.openSubtitlesUsername??"",password:"",clearLogin:!1})},[x,d]);const h=b=>c(T=>T&&{...T,...b}),j=()=>r("save",async()=>{d&&(await i(()=>D.post("/admin/api/subtitle-settings",{bazarrEnabled:d.bazarr,openSubtitlesEnabled:d.openSubtitles,openSubtitlesApiKey:d.key.trim(),clearOpenSubtitlesApiKey:d.clearKey,openSubtitlesUsername:d.username.trim(),openSubtitlesPassword:d.password,clearOpenSubtitlesLogin:d.clearLogin}),"Subtitle settings saved."),c(null),await a())}),y=()=>r("test",async()=>{u(null);const b=await i(()=>D.post("/admin/api/subtitle-test"));u((b==null?void 0:b.results)??[])}),v=()=>r("clear",async()=>{await i(()=>D.post("/admin/api/subtitle-settings",{action:"clear-stored"}),"Stored subtitles deleted."),m(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from."}),e.jsx(_,{message:n}),t||!x||!d?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"offered on televisions",value:x.available?"yes":"no",small:!0,icon:"captions",tone:x.available?"ok":void 0},{label:"providers on",value:w((x.bazarrEnabled&&x.bazarrConfigured?1:0)+(x.openSubtitlesEnabled?1:0)),icon:"list",tone:"note"},{label:"subtitles held",value:w((f==null?void 0:f.count)??0),icon:"database",tone:"data"},{label:"last fetched",value:F(f==null?void 0:f.latest),small:!0,icon:"clock"}]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"Bazarr",intro:"Bazarr writes the subtitle file beside the media file, so Emby finds it and the track behaves like one that was always there. Its address is deployment configuration; this switch only decides whether viewers may use it.",icon:"wrench",tone:"data",actions:x.bazarrConfigured?d.bazarr?e.jsx(N,{tone:"ok",children:"on"}):e.jsx(N,{children:"off"}):e.jsx(N,{children:"not configured"}),footer:e.jsx("span",{className:"hint",children:x.bazarrConfigured?`Configured at ${x.bazarrUrl}`:"Set MEMBY_BAZARR_URL and MEMBY_BAZARR_API_KEY to use Bazarr."}),children:e.jsx(G,{label:"Offer Bazarr in the player",hint:"Off leaves every subtitle it has already written in place.",checked:d.bazarr,disabled:!x.bazarrConfigured,onChange:b=>h({bazarr:b})})}),e.jsxs(M,{title:"OpenSubtitles",intro:"OpenSubtitles hands back a file rather than writing one, so Memby keeps what it fetches and serves it to the television itself. Titles are matched on their IMDb or TMDb id, which is exact — there is no guessing at a name.",icon:"captions",tone:"note",actions:x.openSubtitlesEnabled?e.jsx(N,{tone:x.openSubtitlesAccount?"ok":"warn",children:x.openSubtitlesAccount?"on · signed in":"on · anonymous"}):e.jsx(N,{children:x.openSubtitlesKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(G,{label:"Offer OpenSubtitles in the player",hint:"Needs an API key. It cannot be switched on without one.",checked:d.openSubtitles,onChange:b=>h({openSubtitles:b})}),e.jsx(q,{label:"API key",hint:"From your consumer at opensubtitles.com. Leave blank to keep the saved key.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:d.key,placeholder:x.openSubtitlesKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:b=>h({key:b.target.value})})}),e.jsx(G,{label:"Remove the saved key",checked:d.clearKey,onChange:b=>h({clearKey:b})}),e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Account username",hint:"Optional, and the difference between a working feature and one that stops after a few files: without an account, downloads come out of the small anonymous allowance.",children:e.jsx("input",{type:"text",autoComplete:"off",value:d.username,placeholder:"Not signed in",onChange:b=>h({username:b.target.value})})}),e.jsx(q,{label:"Account password",hint:"Leave blank to keep the saved one.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:d.password,onChange:b=>h({password:b.target.value})})})]}),e.jsx(G,{label:"Sign out and forget the account",checked:d.clearLogin,onChange:b=>h({clearLogin:b})})]})]}),e.jsxs(M,{children:[e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>void j(),children:"Save subtitle settings"}),e.jsx($,{busy:l==="test",icon:"pulse",onClick:()=>void y(),children:"Test the providers"}),e.jsx("span",{className:"hint",children:x.featureEnabled?"A change applies to the next title opened; no app release is required.":"Downloading subtitles is switched off on the Features page, so nothing here is offered."})]}),o===null?null:o.length===0?e.jsx(ee,{children:"No provider is switched on, so there was nothing to ask."}):e.jsx("div",{className:"list",children:o.map(b=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:b.provider}),e.jsx("p",{children:b.message})]}),e.jsx("div",{className:"list-actions",children:e.jsx(N,{tone:b.ok?"ok":"bad",children:b.ok?"reachable":"not reachable"})})]},b.provider))})]}),e.jsx(M,{title:"Subtitles Memby is holding",intro:"Only files fetched from a provider that cannot write beside the media file are kept here; they are served to televisions as ordinary tracks on every later playback. Emptying this is safe — each one can be fetched again, at the cost of the download allowance that fetched it.",icon:"database",tone:"data",actions:f!=null&&f.count?e.jsxs(N,{tone:"data",children:[w(f.count)," files · ",he(f.bytes)]}):e.jsx(N,{children:"nothing held"}),footer:e.jsx($,{variant:"danger",disabled:!(f!=null&&f.count),onClick:()=>m(!0),children:"Delete every stored subtitle"}),children:e.jsx(e.Fragment,{})})]}),p?e.jsx(ke,{title:"Delete every stored subtitle?",body:"Each one can be fetched again, at the cost of the download allowance that fetched it. Subtitles Bazarr wrote beside the media are untouched — those belong to Emby.",confirmLabel:"Delete",destructive:!0,busy:l==="clear",onConfirm:()=>void v(),onCancel:()=>m(!1)}):null]})}function Gt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(null),[o,u]=g.useState(!1),p=s==null?void 0:s.updatePolicy;g.useEffect(()=>{d||!p||c({version:p.latestVersion??"",url:p.downloadUrl??"",notes:p.notes??"",retireBelow:p.retireBelowVersion??"",required:!!p.minimumVersion&&p.minimumVersion===p.latestVersion,destructive:!!p.retireBelowVersion&&p.retireBelowVersion===p.latestVersion})},[p,d]);const m=j=>r(j?"save":"off",async()=>{d&&(await i(()=>D.post("/admin/api/update-policy",{enabled:j,latestVersion:d.version.trim(),downloadUrl:d.url.trim(),notes:d.notes.trim(),required:d.required,destructive:d.destructive,retireBelowVersion:d.retireBelow.trim()}),j?"Update policy saved.":"Update prompts turned off."),u(!1),c(null),await a())}),x=!!(p!=null&&p.minimumVersion)&&(p==null?void 0:p.minimumVersion)===(p==null?void 0:p.latestVersion),f=!!(p!=null&&p.retireBelowVersion)&&(p==null?void 0:p.retireBelowVersion)===(p==null?void 0:p.latestVersion),h=j=>c(y=>y&&{...y,...j});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"App updates",intro:"Publish an optional or a required client update."}),e.jsx(_,{message:n}),t||!d?e.jsx(V,{rows:1}):e.jsxs(M,{title:"Update policy",intro:"Televisions check on every launch. An optional update is a prompt the viewer can dismiss; a required one covers the home screen until they update, so it needs a download URL that actually works.",icon:"download",tone:"info",actions:p!=null&&p.enabled?e.jsxs(N,{tone:x?"warn":"ok",children:[f?"sign-out · ":x?"required · ":"optional · ",p.latestVersion]}):e.jsx(N,{children:"off"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>d.required?u(!0):void m(!0),children:"Save policy"}),e.jsx($,{busy:l==="off",onClick:()=>void m(!1),children:"Turn prompts off"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Latest version",children:e.jsx("input",{type:"text",value:d.version,placeholder:"0.2.63",onChange:j=>h({version:j.target.value})})}),e.jsx(q,{label:"APK URL",children:e.jsx("input",{type:"text",value:d.url,placeholder:"https://nas/memby/memby-0.2.63.apk",onChange:j=>h({url:j.target.value})})})]}),e.jsx(q,{label:"What's new",hint:"Shown on the television above the update button.",children:e.jsx("input",{type:"text",value:d.notes,placeholder:"One line the viewer reads",onChange:j=>h({notes:j.target.value})})}),e.jsx(q,{label:"Sign out builds below",hint:"The destructive compatibility floor. Leave blank to keep every supported viewer signed in.",children:e.jsx("input",{type:"text",value:d.retireBelow,placeholder:"0.2.44",onChange:j=>h({retireBelow:j.target.value})})}),e.jsx(G,{label:"Require this update",hint:"Blocks the home screen on every television below this version.",checked:d.required,onChange:j=>h({required:j})}),e.jsx(G,{label:"Set the destructive floor to this update",hint:"Deletes sessions on every older television when it next uses Memby, then shows the required update screen.",checked:d.destructive,onChange:j=>h(j?{destructive:!0,required:!0,retireBelow:d.version.trim()}:{destructive:!1,retireBelow:d.retireBelow.trim()===d.version.trim()?"":d.retireBelow})})]}),o&&d?e.jsx(ke,{title:d.destructive?"Sign every older television out?":"Require this update?",body:d.destructive?"This deletes sessions on every older television and forces viewers to sign in again after updating.":"Required updates block the home screen on every television below this version until they update.",confirmLabel:"Publish",destructive:d.destructive,busy:l==="save",onConfirm:()=>void m(!0),onCancel:()=>u(!1)}):null]})}const Zt=[{value:60,label:"Every minute"},{value:300,label:"Every 5 minutes"},{value:600,label:"Every 10 minutes"},{value:900,label:"Every 15 minutes"},{value:1800,label:"Every 30 minutes"},{value:3600,label:"Hourly"},{value:10800,label:"Every 3 hours"},{value:21600,label:"Every 6 hours"},{value:43200,label:"Every 12 hours"},{value:86400,label:"Daily"},{value:604800,label:"Weekly"}];function Yt(s){const n=[...Zt];for(const t of[s.defaultIntervalSeconds,s.intervalSeconds])t>0&&!n.some(a=>a.value===t)&&n.push({value:t,label:Ne(t).replace(/^every /,"Every ")});return n.sort((t,a)=>t.value-a.value)}const Ge=s=>s.defaultIntervalSeconds>0&&s.intervalSeconds!==s.defaultIntervalSeconds;function us(s){return s==="failed"?"bad":s==="running"?"info":s==="skipped"?"warn":"ok"}function ls(s){if(s.running)return{state:"running",label:"Running",tone:"info",title:"Running right now"};if(!s.enabled)return{state:"disabled",label:"Disabled",tone:"warn",title:"Switched off — it will not run on its schedule"};const n=s.lastRun;return n?n.status==="failed"?{state:"failed",label:"Failed",tone:"bad",title:n.error||"The last run failed"}:n.status==="skipped"?{state:"skipped",label:"Skipped",tone:"warn",title:n.detail||"The last run declined to do anything"}:{state:"successful",label:"Successful",tone:"ok",title:"The last run finished cleanly"}:{state:"never",label:"Never run",tone:void 0,title:"This task has not run since the gateway started recording"}}function Jt(s){if(s.state==="running"||s.state==="successful")return"ok";if(s.state==="failed")return"bad";if(s.state==="disabled"||s.state==="skipped")return"warn"}const Gs={failed:0,running:1,skipped:2,disabled:3,never:4,successful:5};function Qt(s,n){const t=Gs[ls(s).state]-Gs[ls(n).state];if(t!==0)return t;if(!!s.group!=!!n.group)return s.group?-1:1;const a=s.group.localeCompare(n.group);return a!==0?a:s.name.localeCompare(n.name)}const Xt=2e4,ei=3e3;function si(){const{wrap:s}=ae(),{busy:n,run:t}=ne(),[a,i]=g.useState(!1),{data:l,error:r,loading:d,reload:c}=J("/admin/api/tasks?limit=60",{pollMs:a?ei:Xt}),o=(l==null?void 0:l.tasks)??[],u=o.some(j=>j.running);u!==a&&i(u);const p=j=>t(j.id,async()=>{await s(()=>D.post(`/admin/api/tasks/${encodeURIComponent(j.id)}/run`),`${j.name} started.`),await c()}),m=o.filter(j=>{var y;return((y=j.lastRun)==null?void 0:y.status)==="failed"}).length,x=o.filter(Ge).length,f=o.filter(j=>!j.enabled).length,h=[...o].sort(Qt);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Scheduled tasks",intro:"The gateway's background work: what it does, when it last ran, how long it took and whether it worked. Every one of these can be started by hand."}),e.jsx(_,{message:r}),d?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"Tasks",value:w(o.length),icon:"clock",tone:"info"},{label:"Running now",value:w(o.filter(j=>j.running).length),icon:"pulse",tone:u?"ok":void 0},{label:"Last run failed",value:w(m),icon:"alert",tone:m>0?"bad":void 0},{label:"Switched off",value:w(f),icon:"power",tone:f>0?"warn":void 0},{label:"Retimed",value:w(x),icon:"clock",tone:x>0?"note":void 0}]}),m>0?e.jsx(pe,{tone:"bad",children:"A failed task publishes an administrative event, so the failure is in the activity feed and wherever your integrations send it — you did not have to be looking at this page."}):null,e.jsx(M,{title:"Tasks",intro:"Anything wrong sorts to the top. Open a task to change its schedule, switch it off, or read its own run history.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Service"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Status"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:h.length===0?e.jsx(Y,{columns:8,children:"This gateway has no scheduled tasks registered."}):h.map(j=>{const y=ls(j),v=j.lastRun,b=`/admin/tasks/${encodeURIComponent(j.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(Z,{className:"table-row-link",to:b,children:j.name}),e.jsx("span",{className:"table-sub",children:j.description})]}),e.jsx("td",{className:"muted nowrap",children:j.group||"Other"}),e.jsxs("td",{className:"nowrap",children:[Ne(j.intervalSeconds),Ge(j)?e.jsxs("span",{className:"table-sub",children:["default ",Ne(j.defaultIntervalSeconds)]}):null]}),e.jsxs("td",{className:"nowrap muted",title:v?F(v.startedAt):void 0,children:[v?be(v.startedAt):"never",v!=null&&v.error?e.jsx("span",{className:"table-sub",children:v.error}):null]}),e.jsx("td",{className:"num muted",children:v?ye(v.durationMs):"—"}),e.jsx("td",{className:"nowrap muted",children:j.enabled?j.running?"now":ds(j.nextRun):"—"}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":Jt(y)}),e.jsx(N,{tone:y.tone,children:e.jsx("span",{title:y.title,children:y.label})})]})}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx($,{size:"sm",icon:"play",busy:n===j.id,disabled:j.running,title:`Run ${j.name} now`,onClick:()=>void p(j)}),e.jsx(Z,{className:"table-row-link",to:b,children:"Details"})]})})]},j.id)})})]})})}),e.jsx(M,{title:"Recent runs",intro:"Every task together and in order, which is what shows two jobs interfering with each other.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Detail"})]})}),e.jsx("tbody",{children:((l==null?void 0:l.runs.length)??0)===0?e.jsx(Y,{columns:6,children:"No task has run yet."}):l==null?void 0:l.runs.map(j=>{var y;return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(j.startedAt),children:be(j.startedAt)}),e.jsx("td",{children:e.jsx(Z,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(j.taskId)}`,children:((y=o.find(v=>v.id===j.taskId))==null?void 0:y.name)??j.taskId})}),e.jsx("td",{className:"muted",children:j.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(j.status),children:j.status})}),e.jsx("td",{className:"num muted",children:ye(j.durationMs)}),e.jsx("td",{className:"muted",children:j.error||j.detail||"—"})]},j.id)})})]})})})]})]})}const ni=2e4,ti=3e3,ii=100;function ai(s){const n=s.filter(l=>l.status!=="running"),t=n.filter(l=>l.status==="failed").length,a=n.filter(l=>l.durationMs>0),i=a.length?a.reduce((l,r)=>l+r.durationMs,0)/a.length:0;return{runs:n.length,failed:t,averageMs:i,timed:a.length}}function ri(){var y;const{taskId:s=""}=Be(),{wrap:n}=ae(),{busy:t,run:a}=ne(),[i,l]=g.useState(!1),{data:r,error:d,loading:c,reload:o}=J(`/admin/api/tasks?task=${encodeURIComponent(s)}&limit=${ii}`,{pollMs:i?ti:ni,enabled:!!s}),u=r==null?void 0:r.tasks.find(v=>v.id===s),p=!!(u!=null&&u.running);p!==i&&l(p);const m=(r==null?void 0:r.runs)??[],x=u?ls(u):void 0,f=ai(m),h=(v,b,T)=>a(v,async()=>{await n(()=>D.put(`/admin/api/tasks/${encodeURIComponent(s)}`,T),b),await o()}),j=v=>a("run",async()=>{await n(()=>D.post(`/admin/api/tasks/${encodeURIComponent(s)}/run`),`${v.name} started.`),await o()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:(u==null?void 0:u.name)||s,intro:u==null?void 0:u.description,icon:"clock",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(Z,{to:"/admin/tasks",children:"Scheduled tasks"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:(u==null?void 0:u.name)||s})]}),actions:u?e.jsx($,{variant:"primary",icon:"play",busy:t==="run",disabled:u.running,onClick:()=>void j(u),children:u.running?"Running":"Run now"}):void 0}),e.jsx(_,{message:d}),c?e.jsx(V,{}):u?e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"Status",value:(x==null?void 0:x.label)??"—",small:!0,icon:"pulse",tone:x==null?void 0:x.tone},{label:"Service",value:u.group||"Other",small:!0,icon:"chip",tone:"info"},{label:"Runs every",value:Ne(u.intervalSeconds),small:!0,icon:"clock",tone:Ge(u)?"note":void 0},{label:"Next run",value:u.enabled?u.running?"now":ds(u.nextRun):"not scheduled",small:!0,icon:"history",tone:u.enabled?void 0:"warn"},{label:`Failed of the last ${w(f.runs)}`,value:w(f.failed),icon:"alert",tone:f.failed>0?"bad":void 0},{label:`Average of ${w(f.timed)} timed runs`,value:f.timed?ye(Math.round(f.averageMs)):"—",small:!0,icon:"chart",tone:"data"}]}),(y=u.lastRun)!=null&&y.error?e.jsxs(pe,{tone:"bad",children:["The last run failed: ",u.lastRun.error]}):null,e.jsxs(M,{title:"Schedule",intro:"How often the gateway runs this on its own, and whether it runs it at all.",icon:"sliders",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx(q,{label:"How often it runs",children:e.jsx("select",{"aria-label":`How often ${u.name} runs`,value:u.intervalSeconds,disabled:t==="interval"||u.running,onChange:v=>void h("interval",`${u.name} now runs ${Ne(Number(v.target.value))}.`,{intervalSeconds:Number(v.target.value)}),children:Yt(u).map(v=>e.jsxs("option",{value:v.value,children:[v.label,v.value===u.defaultIntervalSeconds?" (default)":""]},v.value))})}),Ge(u)?e.jsxs($,{icon:"refresh",busy:t==="interval",disabled:u.running,onClick:()=>void h("interval",`${u.name} back to its default cadence.`,{intervalSeconds:0}),children:["Back to ",Ne(u.defaultIntervalSeconds)]}):null]}),e.jsx(G,{label:"Run this on its schedule",hint:"Switched off, the gateway leaves it alone. You can still start it by hand.",checked:u.enabled,disabled:t==="enabled",onChange:v=>void h("enabled",v?`${u.name} switched on.`:`${u.name} switched off.`,{enabled:v})}),Ge(u)?e.jsxs(pe,{tone:"note",children:["This task is retimed: its code asks for ",Ne(u.defaultIntervalSeconds)," and it is set to ",Ne(u.intervalSeconds),"."]}):null]}),e.jsx(M,{title:"Run history",intro:"This task alone, newest first — which is what separates a job that fails occasionally from one that has stopped working.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Detail"})]})}),e.jsx("tbody",{children:m.length===0?e.jsx(Y,{columns:5,children:"This task has not run yet."}):m.map(v=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(v.startedAt),children:be(v.startedAt)}),e.jsx("td",{className:"muted",children:v.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(v.status),children:v.status})}),e.jsx("td",{className:"num muted",children:ye(v.durationMs)}),e.jsx("td",{className:"muted",children:v.error||v.detail||"—"})]},v.id))})]})})})]}):e.jsx(M,{title:"No such task",icon:"alert",tone:"bad",children:e.jsxs(ee,{children:["This gateway has no task called ",e.jsx("code",{children:s}),". It may have been renamed or removed."," ",e.jsx(Z,{to:"/admin/tasks",children:"Back to scheduled tasks"}),"."]})})]})}function Pe(s){switch(s){case"error":return"bad";case"healthy":return"ok";case"running":return"info";case"disabled":return"warn";default:return}}function An(s){return Pe(s)}const Zs={error:0,running:1,idle:2,healthy:3,disabled:4,unconfigured:5};function li(s,n){const t=Zs[s.status]-Zs[n.status];return t!==0?t:s.name.localeCompare(n.name)}const oi=s=>!!(s&&(s.processed||s.changed||s.skipped||s.failed));function Je(s){if(!oi(s)||!s)return"";const n=[];return s.processed&&n.push(`${s.processed.toLocaleString()} checked`),s.changed&&n.push(`${s.changed.toLocaleString()} changed`),s.skipped&&n.push(`${s.skipped.toLocaleString()} skipped`),s.failed&&n.push(`${s.failed.toLocaleString()} failed`),n.join(" · ")}function Ms(s){if(s.status==="running")return"Running…";if(s.error)return s.error;if(s.detail)return s.detail;const n=Je(s.counts);return n||"No changes"}function ci(s){return s.powers.length?`Switching ${s.name} off also stops: ${s.powers.join("; ")}.`:""}const di=2e4,hi=3e3;function ui(){const[s,n]=g.useState(!1),{data:t,error:a,loading:i}=J("/admin/api/integrations/services?limit=40",{pollMs:s?hi:di}),l=((t==null?void 0:t.services)??[]).some(m=>m.running);l!==s&&n(l);const r=[...(t==null?void 0:t.services)??[]].sort(li),d=(t==null?void 0:t.runs)??[],c=r.filter(m=>m.status==="error"),o=r.filter(m=>m.running),u=r.filter(m=>m.configured&&!m.enabled),p=r.filter(m=>!m.configured);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Integrations",intro:"The services Memby depends on: whether each one is configured, whether it is working, what it is doing right now and what it last managed to do."}),e.jsx(_,{message:a}),i?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"Integrations",value:w(r.length),icon:"plug",tone:"info"},{label:"Not working",value:w(c.length),icon:"alert",tone:c.length>0?"bad":void 0},{label:"Running now",value:w(o.length),icon:"pulse",tone:o.length>0?"ok":void 0},{label:"Switched off",value:w(u.length),icon:"power",tone:u.length>0?"warn":void 0},{label:"Not configured",value:w(p.length),icon:"sliders",tone:p.length>0?"note":void 0}]}),c.length>0?e.jsxs(pe,{tone:"bad",children:[c.map(m=>m.name).join(", ")," ",c.length===1?"is not answering":"are not answering",". A failed run also publishes an administrative event, so it is in the activity feed and wherever your webhooks send it — you did not have to be looking at this page."]}):null,e.jsx(M,{title:"Services",intro:"Anything wrong sorts to the top. Open a service to switch it off, read its settings, or see everything it has done.",icon:"plug",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Integration"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Enabled"})]})}),e.jsx("tbody",{children:r.length===0?e.jsx(Y,{columns:6,children:"This gateway has no integrations."}):r.map(m=>e.jsx(mi,{service:m},m.id))})]})})}),e.jsx(M,{title:"Recent activity",intro:"Every integration's work together and in order, which is what shows two of them getting in each other's way. This is what Memby attempted and what came of it — the Logs page is where the technical detail behind a failure lives.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Integration"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:d.length===0?e.jsx(Y,{columns:6,children:"No integration has run yet."}):d.map(m=>{const x=r.find(h=>h.id===m.integrationId),f=x==null?void 0:x.tasks.find(h=>h.id===m.taskId);return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(m.startedAt),children:be(m.startedAt)}),e.jsx("td",{className:"nowrap",children:e.jsx(Z,{className:"table-row-link",to:`/admin/integrations/${encodeURIComponent(m.integrationId??"")}`,children:(x==null?void 0:x.name)??m.integrationId})}),e.jsx("td",{className:"muted",children:(f==null?void 0:f.name)??m.taskId}),e.jsx("td",{children:e.jsx(N,{tone:us(m.status),children:m.status})}),e.jsxs("td",{className:"muted",children:[Ms(m),m.error&&Je(m.counts)?e.jsx("span",{className:"table-sub",children:Je(m.counts)}):null]}),e.jsx("td",{className:"num muted",children:ye(m.durationMs)})]},m.id)})})]})})})]})]})}function mi({service:s}){const n=`/admin/integrations/${encodeURIComponent(s.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(Z,{className:"table-row-link",to:n,children:s.name}),e.jsx("span",{className:"table-sub",children:s.address||s.summary})]}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":An(s.status)}),e.jsx(N,{tone:Pe(s.status),children:e.jsx("span",{title:s.detail||s.statusLabel,children:s.statusLabel})})]})}),e.jsx("td",{className:"nowrap muted",title:s.lastRun?F(s.lastRun.startedAt):void 0,children:s.lastRun?be(s.lastRun.startedAt):"never"}),e.jsx("td",{className:"muted",children:s.lastRun?Ms(s.lastRun):s.detail||"—"}),e.jsx("td",{className:"nowrap muted",children:s.configured?s.enabled?s.running?"now":ds(s.nextRun):"off":"—"}),e.jsx("td",{className:"nowrap",children:s.configured?s.enabled?e.jsx(N,{tone:"ok",children:"On"}):e.jsx(N,{tone:"warn",children:"Off"}):e.jsx(N,{children:"not configured"})})]})}const pi={imdb:"IMDb",tomatoes:"Rotten Tomatoes",audience:"Rotten Tomatoes Audience",metacritic:"Metacritic",letterboxd:"Letterboxd",rogerebert:"Roger Ebert",tmdb:"TMDb",trakt:"Trakt",mal:"MyAnimeList",anilist:"AniList",anidb:"AniDB",kitsu:"Kitsu",score:"MDBList Score",score_average:"MDBList Average"};function xi(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(!1),[o,u]=g.useState(""),[p,m]=g.useState(!1),[x,f]=g.useState([]),[h,j]=g.useState(!1),y=s==null?void 0:s.mdblist;g.useEffect(()=>{h||!y||(c(y.enabled),f(y.sources??[]))},[y,h]);const v=()=>r("save",async()=>{await i(()=>D.post("/admin/api/mdblist-settings",{enabled:d,apiKey:o.trim(),clearApiKey:p,sources:x}),"Ratings settings saved."),u(""),m(!1),j(!1),await a()}),b=(y==null?void 0:y.cachedTitles)??0;return e.jsxs(e.Fragment,{children:[e.jsx(_,{message:n}),t||!y?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsxs(xe,{cols:"2",children:[e.jsxs(M,{title:"MDBList connection",intro:"The key stays on this server and a failure never blocks a television. Every rating fetched is stored here permanently and re-checked about once a month, so browsing the library costs nothing after the first look at a title.",icon:"star",tone:"note",actions:d?e.jsxs(N,{tone:"ok",children:["on · ",x.length," sources"]}):e.jsx(N,{children:y.apiKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(G,{label:"Show external ratings on televisions",hint:"Off leaves the stored ratings in place.",checked:d,onChange:T=>{c(T),j(!0)}}),e.jsx(q,{label:"API key",hint:"Leave blank to keep the key that is already saved.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:o,placeholder:y.apiKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:T=>u(T.target.value)})}),e.jsx(G,{label:"Remove the saved key",checked:p,onChange:m})]}),e.jsx(M,{title:"Sources shown on televisions",intro:"A title with none of these has no ratings strip at all, which is the honest answer — nothing stands in for a score that was never fetched.",icon:"list",tone:"data",children:(y.availableSources??[]).length===0?e.jsx(ee,{children:"No rating sources are available."}):e.jsx("div",{className:"checks columns",children:(y.availableSources??[]).map(T=>e.jsx(G,{label:pi[T]??T,checked:x.includes(T),onChange:k=>{j(!0),f(I=>k?[...I,T]:I.filter(E=>E!==T))}},T))})})]}),e.jsx(M,{children:e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>void v(),children:"Save ratings settings"}),e.jsx("span",{className:"hint",children:b?"Ratings are fetched as televisions browse, never on the request path.":"No ratings stored yet. They are saved as televisions browse the library."})]})})]})]})}const ji={id:"",name:"Discord",url:"",enabled:!0,events:[]};function gi(){const{wrap:s,show:n}=ae(),{busy:t,run:a}=ne(),{data:i,error:l,loading:r,reload:d}=J("/admin/api/integrations",{pollMs:6e4}),[c,o]=g.useState(null),[u,p]=g.useState(null),m=(i==null?void 0:i.catalogue)??[],x=(i==null?void 0:i.integrations)??[],f=v=>o({id:v.id,name:v.name,url:"",enabled:v.enabled,events:v.events??[]}),h=()=>a("save",async()=>{if(!c)return;await s(()=>D.post("/admin/api/integrations",c),c.id?"Integration saved.":"Integration added.")&&(o(null),await d())}),j=v=>a("remove",async()=>{await s(()=>D.del(`/admin/api/integrations/${encodeURIComponent(v.id)}`),`${v.name} removed.`),p(null),await d()}),y=v=>a(`test:${v.id}`,async()=>{const b=await s(()=>D.post(`/admin/api/integrations/${encodeURIComponent(v.id)}/test`));b&&n(b.message,b.ok?"ok":"bad"),await d()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Event webhooks",intro:"Send administrative events to somewhere you already look. Events pass through the gateway's own event layer, so nothing about authentication or scheduled tasks knows Discord exists — and a second kind of destination is a change here rather than everywhere.",actions:e.jsx($,{variant:"primary",icon:"plus",onClick:()=>o(ji),children:"Add a webhook"})}),e.jsx(_,{message:l}),((i==null?void 0:i.dropped)??0)>0?e.jsxs(pe,{tone:"warn",children:[w((i==null?void 0:i.dropped)??0)," events could not be queued for delivery. The queue is deliberately lossy — a slow endpoint must never hold up a television signing in — but a number growing here means a destination is not keeping up."]}):null,r?e.jsx(V,{}):x.length===0&&!c?e.jsx(M,{title:"Nothing configured",icon:"plug",tone:"note",children:e.jsx(ee,{children:"No destinations yet. A Discord webhook takes about a minute: in Discord, open a channel's settings → Integrations → Webhooks → New Webhook, copy its URL, and paste it here."})}):x.map(v=>e.jsx(fi,{integration:v,catalogue:m,busy:t,onEdit:()=>f(v),onTest:()=>void y(v),onRemove:()=>p(v)},v.id)),c?e.jsx(yi,{draft:c,catalogue:m,busy:t==="save",onChange:o,onSave:()=>void h(),onCancel:()=>o(null)}):null,u?e.jsx(ke,{title:`Remove ${u.name}?`,body:"The webhook address and its delivery history go with it. Events already published stay in the activity feed.",confirmLabel:"Remove",destructive:!0,busy:t==="remove",onConfirm:()=>void j(u),onCancel:()=>p(null)}):null]})}function vi(){const{wrap:s}=ae(),{busy:n,run:t}=ne(),{data:a,error:i,loading:l,reload:r}=J("/admin/api/sonarr-request-policy"),[d,c]=g.useState(0),[o,u]=g.useState(!1);g.useEffect(()=>{a&&(c(a.qualityProfileId),u(a.searchImmediately))},[a]);const p=()=>t("sonarr-request-policy",async()=>{await s(()=>D.post("/admin/api/sonarr-request-policy",{qualityProfileId:d,searchImmediately:o}),"Sonarr TV request policy saved."),await r()}),m=a==null?void 0:a.profiles.find(x=>x.id===d);return e.jsxs(M,{title:"Sonarr TV requests",intro:"The policy Memby uses when a viewer requests a television series. The series remains monitored; searching its existing episodes is an explicit choice.",icon:"tv",tone:a!=null&&a.configured?"ok":"warn",actions:a!=null&&a.configured?e.jsx(N,{tone:"ok",children:"configured"}):e.jsx(N,{tone:"warn",children:"needs attention"}),footer:e.jsx($,{variant:"primary",busy:n==="sonarr-request-policy",disabled:l||d<=0,onClick:()=>void p(),children:"Save Sonarr policy"}),children:[e.jsx(_,{message:i??(a==null?void 0:a.error)??""}),l?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"fields",children:e.jsx(q,{label:"Request quality profile",hint:"Memby stores this Sonarr profile ID. 720p is the recommended safe default; Memby will never fall back to Any.",children:e.jsxs("select",{value:d,onChange:x=>c(Number(x.target.value)),disabled:!(a!=null&&a.profiles.length),children:[e.jsx("option",{value:0,children:"Choose a quality profile…"}),a==null?void 0:a.profiles.map(x=>e.jsxs("option",{value:x.id,children:[x.name,x.recommended?" — recommended (720p)":""]},x.id))]})})}),e.jsx(G,{label:"Search for episodes immediately after request",hint:"Off adds and monitors the series without searching its backlog. Enable only when requests should start an immediate episode search.",checked:o,onChange:u}),m?e.jsxs(pe,{tone:"info",children:["Requested series will use ",e.jsx("b",{children:m.name})," (profile ID ",m.id,"), be monitored using Memby’s existing all-episodes strategy, and ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function bi(){const{wrap:s}=ae(),{busy:n,run:t}=ne(),{data:a,error:i,loading:l,reload:r}=J("/admin/api/radarr-request-policy"),[d,c]=g.useState(0),[o,u]=g.useState(!1);g.useEffect(()=>{a&&(c(a.qualityProfileId),u(a.searchImmediately))},[a]);const p=()=>t("radarr-request-policy",async()=>{await s(()=>D.post("/admin/api/radarr-request-policy",{qualityProfileId:d,searchImmediately:o}),"Radarr movie request policy saved."),await r()}),m=a==null?void 0:a.profiles.find(x=>x.id===d);return e.jsxs(M,{title:"Radarr movie requests",intro:"The policy Memby uses when a viewer requests a film. The film remains monitored; an immediate Radarr search is an explicit choice.",icon:"tv",tone:a!=null&&a.configured?"ok":"warn",actions:a!=null&&a.configured?e.jsx(N,{tone:"ok",children:"configured"}):e.jsx(N,{tone:"warn",children:"needs attention"}),footer:e.jsx($,{variant:"primary",busy:n==="radarr-request-policy",disabled:l||d<=0,onClick:()=>void p(),children:"Save Radarr policy"}),children:[e.jsx(_,{message:i??(a==null?void 0:a.error)??""}),l?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"fields",children:e.jsx(q,{label:"Request quality profile",hint:"Memby stores this Radarr profile ID. 720p is the recommended safe default; Memby will never fall back to Any.",children:e.jsxs("select",{value:d,onChange:x=>c(Number(x.target.value)),disabled:!(a!=null&&a.profiles.length),children:[e.jsx("option",{value:0,children:"Choose a quality profile…"}),a==null?void 0:a.profiles.map(x=>e.jsxs("option",{value:x.id,children:[x.name,x.recommended?" — recommended (720p)":""]},x.id))]})})}),e.jsx(G,{label:"Search for the film immediately after request",hint:"Off adds and monitors the film without asking Radarr to search. Enable only when requests should start an immediate search.",checked:o,onChange:u}),m?e.jsxs(pe,{tone:"info",children:["Requested films will use ",e.jsx("b",{children:m.name})," (profile ID ",m.id,"), remain monitored, and ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function fi({integration:s,catalogue:n,busy:t,onEdit:a,onTest:i,onRemove:l}){const r=s.health,d=!r.lastFailure||r.lastSuccess&&r.lastSuccess>r.lastFailure,c=s.events??[];return e.jsxs(M,{title:s.name,intro:s.hint?`Discord webhook ${s.hint}`:"Discord webhook",icon:"plug",tone:s.enabled?"ok":"warn",actions:e.jsxs(e.Fragment,{children:[s.enabled?e.jsx(N,{tone:"ok",children:"on"}):e.jsx(N,{tone:"warn",children:"off"}),r.deliveries>0?e.jsx(N,{tone:d?"ok":"bad",children:d?"delivering":"failing"}):e.jsx(N,{children:"never used"}),e.jsx($,{size:"sm",icon:"pulse",busy:t===`test:${s.id}`,onClick:i,children:"Test"}),e.jsx($,{size:"sm",onClick:a,children:"Edit"}),e.jsx($,{size:"sm",variant:"danger",icon:"trash",onClick:l,title:"Remove"})]}),children:[e.jsxs("div",{className:"list",children:[e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Events sent"}),e.jsx("p",{children:c.length===0?"None selected — this destination is configured but will never post anything.":c.map(o=>{var u;return((u=n.find(p=>p.type===o))==null?void 0:u.label)??o}).join(", ")})]})}),e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last delivered"}),e.jsx("p",{children:r.lastSuccess?F(r.lastSuccess):"never"})]}),e.jsx("div",{className:"list-actions",children:r.deliveries>0?e.jsxs("span",{className:"quiet",children:[w(r.deliveries)," attempts, ",w(r.failures)," failed"]}):null})]}),r.lastFailure?e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last failure"}),e.jsxs("p",{children:[F(r.lastFailure),r.lastError?` — ${r.lastError}`:""]})]})}):null]}),s.deliveries.length>0?e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Attempted"}),e.jsx("th",{children:"Event"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:s.deliveries.map(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(o.attemptedAt),children:be(o.attemptedAt)}),e.jsx("td",{className:"muted",children:o.eventType}),e.jsx("td",{children:o.success?e.jsx(N,{tone:"ok",children:o.statusCode||"ok"}):e.jsx(N,{tone:"bad",children:o.error||o.statusCode||"failed"})}),e.jsx("td",{className:"num muted",children:ye(o.durationMs)})]},o.id))})]})}):e.jsx(K,{children:e.jsx("table",{children:e.jsx("tbody",{children:e.jsx(Y,{columns:4,children:"Nothing has been delivered through this webhook yet."})})})})]})}function yi({draft:s,catalogue:n,busy:t,onChange:a,onSave:i,onCancel:l}){const r=[...new Set(n.map(c=>c.group))],d=(c,o)=>a({...s,events:o?[...s.events,c]:s.events.filter(u=>u!==c)});return e.jsxs(M,{title:s.id?`Edit ${s.name}`:"New Discord webhook",icon:"plug",tone:"info",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:t,onClick:i,children:s.id?"Save":"Add"}),e.jsx($,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx("span",{className:"spacer"}),s.events.length===0?e.jsx("span",{className:"quiet",children:"Nothing selected — this destination would never post."}):e.jsxs("span",{className:"quiet",children:[s.events.length," events selected"]})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Name",hint:"What this destination is called in the console.",children:e.jsx("input",{type:"text",value:s.name,onChange:c=>a({...s,name:c.target.value})})}),e.jsx(q,{label:"Webhook address",hint:s.id?"Leave blank to keep the address already saved — it is a credential and is never sent back to this page.":"Discord → channel settings → Integrations → Webhooks → New Webhook → Copy Webhook URL.",children:e.jsx("input",{type:"url",value:s.url,placeholder:s.id?"unchanged":"https://discord.com/api/webhooks/…",onChange:c=>a({...s,url:c.target.value})})})]}),e.jsx(G,{label:"Enabled",hint:"Off keeps the configuration and stops the posts.",checked:s.enabled,onChange:c=>a({...s,enabled:c})}),r.map(c=>e.jsxs("div",{children:[e.jsx("div",{className:"card-head",style:void 0,children:e.jsx("div",{className:"card-head-text",children:e.jsx("h2",{children:c})})}),n.filter(o=>o.group===c).map(o=>e.jsx(G,{label:o.label,hint:o.description,checked:s.events.includes(o.type),onChange:u=>d(o.type,u)},o.type))]},c))]})}const wi=2e4,ki=3e3;function Ni(){const{integrationId:s=""}=Be(),{wrap:n,show:t}=ae(),{busy:a,run:i}=ne(),[l,r]=g.useState(!1),{data:d,error:c,loading:o,reload:u}=J(`/admin/api/integrations/services/${encodeURIComponent(s)}?limit=80`,{pollMs:l?ki:wi,enabled:!!s}),p=d==null?void 0:d.service,m=(d==null?void 0:d.runs)??[],x=!!(p!=null&&p.running);x!==l&&r(x);const f=v=>i("enabled",async()=>{await n(()=>D.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/enabled`,{enabled:v}),`${(p==null?void 0:p.name)??"Integration"} ${v?"enabled":"disabled"}.`),await u()}),h=()=>i("test",async()=>{const v=await n(()=>D.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/test`));v&&t(v.reachable?`${p==null?void 0:p.name} answered in ${ye(v.latencyMs)}.`:v.error||`${p==null?void 0:p.name} did not answer.`,v.reachable?"ok":"bad"),await u()}),j=(v,b)=>i(v,async()=>{await n(()=>D.post(`/admin/api/tasks/${encodeURIComponent(v)}/run`),`${b} started.`),await u()});if(o||!p)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Integration",intro:"One external service and everything it has done."}),e.jsx(_,{message:c}),o?e.jsx(V,{}):e.jsx(pe,{tone:"warn",children:"No such integration."})]});const y=m.filter(v=>v.status==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:p.name,intro:p.summary,actions:e.jsx(Z,{className:"table-row-link",to:"/admin/integrations",children:"All integrations"})}),e.jsx(_,{message:c}),e.jsx(ie,{tiles:[{label:"Status",value:p.statusLabel,small:!0,icon:"pulse",tone:Pe(p.status)},{label:"Runs recorded",value:w(p.runs),icon:"history",tone:"data"},{label:"Failed runs",value:w(p.failures),icon:"alert",tone:p.failures>0?"bad":void 0},{label:"Last worked",value:p.lastSuccessAt?be(p.lastSuccessAt):"never",small:!0,icon:"check",tone:p.lastSuccessAt?"ok":void 0}]}),e.jsxs(M,{title:"Connection",intro:"Address and credentials come from this gateway's environment; the switch is stored on the server and applies to the whole household.",icon:"plug",tone:Pe(p.status)??"info",actions:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":An(p.status)}),e.jsx(N,{tone:Pe(p.status),children:p.statusLabel})]}),footer:p.configured&&p.probed?e.jsx($,{icon:"sync",busy:a==="test",onClick:()=>void h(),children:"Test connection"}):void 0,children:[p.detail?e.jsx(pe,{tone:Pe(p.status),children:p.detail}):null,p.configured?e.jsxs(e.Fragment,{children:[e.jsx(G,{label:`${p.name} enabled`,hint:p.enabled?ci(p):`Memby is not calling ${p.name} or scheduling any of its work.`,checked:p.enabled,disabled:a==="enabled",onChange:v=>void f(v)}),e.jsx(We,{rows:[{label:"Address",value:p.address||"—"},...(p.facts??[]).map(v=>({label:v.label,value:v.tone?e.jsx(N,{tone:v.tone==="warn"?"warn":"ok",children:v.value}):v.value})),{label:"Last checked",value:p.probed?p.health?`${be(p.health.checkedAt)} · ${ye(p.health.latencyMs)}`:"not yet":"Not probed — see below"}]}),p.probed?null:e.jsxs(pe,{children:[p.name," is not probed for reachability: its allowance is bought by the day, and spending a request of it to draw a status would compete with the televisions for the thing being reported on. Its health comes from the run history below."]})]}):e.jsxs(pe,{tone:"warn",children:["This gateway has no address or credential for ",p.name,". Set them in the deployment's environment and restart the container; there is nothing to switch until then."]})]}),p.powers.length>0?e.jsx(M,{title:"What depends on this",intro:"Switching the service off stops all of it. Nothing below fails quietly — it stops being offered.",icon:"journey",tone:"note",children:e.jsx("div",{className:"list",children:p.powers.map(v=>e.jsxs("div",{className:"list-item",children:[e.jsx(Qe,{name:"check",tone:"ok"}),e.jsx("span",{className:"list-body",children:v})]},v))})}):null,e.jsx(Ci,{id:p.id}),e.jsx(M,{title:"Scheduled work",intro:"The background jobs belonging to this service. Cadence and the per-job switch live on each job's own page; this is where you start one by hand.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:p.tasks.length===0?e.jsx(Y,{columns:5,children:"This service has no scheduled work: Memby calls it when a television asks for something rather than on a timer."}):p.tasks.map(v=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(Z,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(v.id)}`,children:v.name}),e.jsx("span",{className:"table-sub",children:v.description})]}),e.jsx("td",{className:"nowrap muted",children:Ne(v.intervalSeconds)}),e.jsx("td",{className:"nowrap muted",title:v.lastRun?F(v.lastRun.startedAt):void 0,children:v.lastRun?be(v.lastRun.startedAt):"never"}),e.jsx("td",{className:"nowrap muted",children:!v.enabled||!p.enabled?"off":v.running?"now":ds(v.nextRun)}),e.jsx("td",{className:"nowrap",children:e.jsx($,{size:"sm",icon:"play",busy:a===v.id,disabled:v.running,title:`Run ${v.name} now`,onClick:()=>void j(v.id,v.name)})})]},v.id))})]})})}),e.jsx(M,{title:"Run history",intro:"What Memby attempted on this service's behalf and what came of it. This is not the application log — a failure here gives the reason, and Logs is where the technical detail behind it lives.",icon:"history",tone:"note",actions:y>0?e.jsx(Z,{className:"table-row-link",to:`/admin/logs?q=${encodeURIComponent(p.id)}`,children:"Open the logs"}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Checked"}),e.jsx("th",{className:"num",children:"Changed"}),e.jsx("th",{className:"num",children:"Skipped"}),e.jsx("th",{className:"num",children:"Failed"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:m.length===0?e.jsxs(Y,{columns:10,children:["Nothing has run for ",p.name," yet."]}):m.map(v=>e.jsx(Si,{run:v,service:p},v.id))})]})})})]})}function Si({run:s,service:n}){var i,l,r,d;const t=n.tasks.find(c=>c.id===s.taskId),a=c=>c?w(c):"—";return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(s.startedAt),children:be(s.startedAt)}),e.jsx("td",{className:"muted",children:(t==null?void 0:t.name)??s.taskId}),e.jsx("td",{className:"muted",children:s.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(s.status),children:s.status})}),e.jsxs("td",{className:"muted",children:[Ms(s),s.error&&Je(s.counts)?e.jsx("span",{className:"table-sub",children:Je(s.counts)}):null]}),e.jsx("td",{className:"num muted",children:a(((i=s.counts)==null?void 0:i.processed)??0)}),e.jsx("td",{className:"num muted",children:a(((l=s.counts)==null?void 0:l.changed)??0)}),e.jsx("td",{className:"num muted",children:a(((r=s.counts)==null?void 0:r.skipped)??0)}),e.jsx("td",{className:"num muted",children:a(((d=s.counts)==null?void 0:d.failed)??0)}),e.jsx("td",{className:"num muted",children:ye(s.durationMs)})]})}function Ci({id:s}){switch(s){case"mdblist":return e.jsx(xi,{});case"sonarr":return e.jsx(vi,{});case"radarr":return e.jsx(bi,{});default:return null}}function Mi(){var X,le,ue,te;const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(""),[o,u]=g.useState(!1),[p,m]=g.useState(!1),[x,f]=g.useState(!1),[h,j]=g.useState("23:00"),[y,v]=g.useState("07:00"),[b,T]=g.useState(""),[k,I]=g.useState(!1),E=!!((X=s==null?void 0:s.maintenance)!=null&&X.enabled);g.useEffect(()=>{var H;!p&&s&&c(((H=s.maintenance)==null?void 0:H.message)??"")},[s,p]),g.useEffect(()=>{k||!(s!=null&&s.quietTime)||(f(s.quietTime.enabled),j(s.quietTime.startTime),v(s.quietTime.endTime),T(s.quietTime.message))},[s,k]);const A=H=>r(H?"on":"off",async()=>{await i(()=>D.post("/admin/api/maintenance",{enabled:H,message:d}),H?"Memby is offline for every television.":"Memby is back online."),u(!1),m(!1),await a()}),L=()=>r("quiet",async()=>{await i(()=>D.post("/admin/api/quiet-time",{enabled:x,startTime:h,endTime:y,message:b}),x?"Quiet time saved.":"Quiet time turned off."),I(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:1}):e.jsx(M,{title:"Gateway availability",intro:"Takes Memby offline for every television, independently of Emby. Sign-in and all content calls answer 503 with the message below, and the television shows it in place of the launcher rows. This console keeps working.",icon:"power",tone:E?"bad":"warn",actions:E?e.jsx(N,{tone:"bad",children:"offline"}):e.jsx(N,{tone:"ok",children:"online"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"danger",disabled:E,onClick:()=>u(!0),children:"Go offline"}),e.jsx($,{disabled:!E,busy:l==="off",onClick:()=>void A(!1),children:"Bring back online"})]}),children:e.jsx(q,{label:"Message shown on the television",hint:"Say what is happening and when it will be back. It is the only thing the viewer is told.",children:e.jsx("input",{type:"text",value:d,placeholder:"Back shortly — upgrading the server",onChange:H=>{c(H.target.value),m(!0)}})})}),t?null:e.jsxs(M,{title:"Quiet time",intro:`Pause new television requests and server background work every day in ${((le=s==null?void 0:s.quietTime)==null?void 0:le.timeZone)??"the server timezone"}. Work already under way finishes safely. The admin console and health checks stay available so the schedule can always be changed.`,icon:"clock",tone:(ue=s==null?void 0:s.quietTime)!=null&&ue.active?"warn":"info",actions:(te=s==null?void 0:s.quietTime)!=null&&te.active?e.jsx(N,{tone:"warn",children:"active now"}):x?e.jsx(N,{tone:"ok",children:"scheduled"}):e.jsx(N,{children:"off"}),footer:e.jsx($,{variant:"primary",busy:l==="quiet",onClick:()=>void L(),children:"Save quiet time"}),children:[e.jsx(G,{label:"Pause server activity during quiet time",checked:x,onChange:H=>{f(H),I(!0)}}),e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Starts",hint:"Uses the server's 24-hour clock.",children:e.jsx("input",{type:"time",value:h,onChange:H=>{j(H.target.value),I(!0)}})}),e.jsx(q,{label:"Ends",hint:"May be on the following day, for example 23:00 to 07:00.",children:e.jsx("input",{type:"time",value:y,onChange:H=>{v(H.target.value),I(!0)}})})]}),e.jsx(q,{label:"Message shown on the television",hint:"Shown when a television contacts Memby during quiet time.",children:e.jsx("input",{type:"text",value:b,placeholder:"Quiet time — try again after 7 am",onChange:H=>{T(H.target.value),I(!0)}})})]}),o?e.jsx(ke,{title:"Take Memby offline?",body:"Every television will stop working immediately and show your message in place of the launcher. This console keeps working.",confirmLabel:"Go offline",destructive:!0,busy:l==="on",onConfirm:()=>void A(!0),onCancel:()=>u(!1)}):null]})}const Ys=s=>s==="Unattributed"?"warn":s==="Go runtime"?"note":"info";function $i(){const s=J("/admin/api/runtime",{pollMs:3e4}),[n,t]=g.useState(),[a,i]=g.useState(!1),[l,r]=g.useState(""),d=g.useCallback(async()=>{i(!0),r("");try{t(await D.get("/admin/api/runtime/goroutines"))}catch(f){r(f instanceof Error?f.message:String(f))}finally{i(!1)}},[]);if(s.loading||!s.data)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Runtime",intro:"What the gateway process is doing."}),e.jsx(_,{message:s.error}),e.jsx(V,{})]});const c=s.data,o=c.memory,u=c.samples??[],p=c.workers??[],m=p.filter(f=>f.state==="running").length,x=o.memoryLimit>0&&o.memoryLimitw(Math.round(f)))}`,value:w(c.goroutines),icon:"pulse",tone:c.goroutineTrend.direction==="rising"?"warn":"info"},{label:"named workers running",value:w(m),icon:"clock",tone:"note"},{label:`heap in use · ${rs(c.heapTrend,he)}`,value:he(o.heapInuse),icon:"chip",tone:o.heapShare>=.75?"warn":"data"},{label:"running for",value:Tn(c.uptimeSeconds),icon:"history",tone:"data"}]}),e.jsxs(xe,{cols:"2",children:[e.jsxs(M,{title:"Memory",intro:"Three different figures that are routinely confused. Heap in use is what the gateway is actually holding. Reserved is what the Go runtime has taken from the operating system on its behalf — always larger, and not a leak. The limit is the one the container is stopped at.",icon:"chip",tone:"data",children:[x?e.jsxs(e.Fragment,{children:[e.jsx(Mn,{value:o.heapInuse,total:o.memoryLimit,tone:o.heapShare>=.9?"bad":o.heapShare>=.75?"warn":"ok"}),e.jsxs("p",{className:"hint",children:["The heap is using ",(o.heapShare*100).toFixed(1),"% of the"," ",he(o.memoryLimit)," limit",o.configuredLimit?` set by GOMEMLIMIT=${o.configuredLimit}`:"","."]})]}):e.jsx(pe,{tone:"warn",children:"No memory limit is set, so the Go runtime will grow until the container is killed by the host. Set GOMEMLIMIT to give it a ceiling to collect against."}),e.jsx(We,{rows:[{label:"Heap currently used",value:he(o.heapInuse)},{label:"Heap allocated to live objects",value:he(o.heapAlloc)},{label:"Heap held but idle",value:he(o.heapIdle)},{label:"Returned to the operating system",value:he(o.heapReleased)},{label:"Goroutine stacks",value:he(o.stackInuse)},{label:"Runtime / system reserved",value:he(o.sys)},{label:"Configured process limit",value:x?he(o.memoryLimit):"none"},{label:"Next collection at",value:he(o.nextGc)}]})]}),e.jsxs(M,{title:"Collection",intro:"How often the garbage collector runs and how long it stops the gateway for. Everything the televisions ask for waits during a pause, so this is the figure that turns a memory problem into a slow one.",icon:"refresh",tone:"note",children:[e.jsx(Cs,{tiles:[{label:"collections",value:w(o.numGc)},{label:"collections an hour",value:o.gcPerHour>0?o.gcPerHour.toFixed(0):"—"},{label:"recent pause",value:`${o.pauseRecentMs.toFixed(1)} ms`},{label:"paused in total",value:`${(o.pauseTotalMs/1e3).toFixed(1)} s`}]}),e.jsx(We,{rows:[{label:"Processors available",value:w(c.gomaxprocs)},{label:"Operating-system threads",value:w(c.threads)},{label:"Processor use",value:c.process.cpuKnown?`${(c.process.cpuPercent??0).toFixed(1)}% of one processor · ${(c.process.cpuSeconds??0).toFixed(0)}s used in total`:"not available on this host"},{label:"Open connections",value:c.process.filesKnown?`${w(c.process.openSockets)} sockets of ${w(c.process.openFiles)} open files${c.process.fileLimit?` · limit ${w(c.process.fileLimit)}`:""}`:"not available on this host"},{label:"Go version",value:c.goVersion}]})]})]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"Goroutines over time",intro:"A single instantaneous count cannot show a leak. This can: a line that climbs and never comes back down is work the gateway is not letting go of.",icon:"chart",tone:"info",children:e.jsx(Js,{samples:u,valueOf:f=>f.goroutines,format:f=>w(f),everySeconds:c.sampleEverySeconds})}),e.jsx(M,{title:"Memory over time",intro:"Heap in use, sampled on the same tick. A saw-tooth that returns to roughly the same floor after each collection is healthy; a floor that keeps rising is not.",icon:"chart",tone:"data",children:e.jsx(Js,{samples:u,valueOf:f=>f.heapInuse,format:he,everySeconds:c.sampleEverySeconds})})]}),e.jsx(M,{title:"Background workers",intro:"Memby's own long-running work, named where it is started rather than guessed at from a stack. A worker that has finished is not necessarily a fault — several are one-shot startup jobs — but one that keeps being started again is failing at something.",icon:"clock",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Worker"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Since"}),e.jsx("th",{className:"num",children:"Starts"})]})}),e.jsx("tbody",{children:p.length===0?e.jsx(Y,{columns:5,children:"Nothing has registered. This gateway predates named workers."}):p.map(f=>e.jsx(Ri,{worker:f},f.name))})]})})}),e.jsxs(M,{title:"Goroutine breakdown",intro:"What those goroutines are actually doing, and which part of Memby they belong to. Collecting it means walking every stack, which stops the gateway for a few milliseconds — so it is taken when you ask rather than continuously.",icon:"search",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx($,{onClick:d,busy:a,icon:"refresh",variant:"primary",children:n?"Collect again":"Collect breakdown"}),e.jsx("a",{href:"/admin/api/runtime/goroutines?format=text",download:!0,children:e.jsx($,{icon:"download",variant:"quiet",children:"Download full stack dump"})})]}),e.jsx(_,{message:l}),n?e.jsx(Ti,{report:n}):e.jsx(ee,{children:"Nothing collected yet. The snapshot is a moment in time, so it is taken on request and stamped with when it was taken."})]})]})}function Ri({worker:s}){const n=s.state==="finished";return e.jsxs("tr",{children:[e.jsx("td",{children:s.name}),e.jsx("td",{children:e.jsx(N,{tone:"info",children:s.component})}),e.jsx("td",{children:e.jsx(N,{tone:n?"note":"ok",children:n?"finished":"running"})}),e.jsx("td",{children:F(n?s.stopped:s.started)}),e.jsx("td",{className:"num",children:s.starts>1?e.jsx(N,{tone:"warn",children:s.starts}):s.starts})]})}function Js({samples:s,valueOf:n,format:t,everySeconds:a}){if(s.length<2)return e.jsxs(ee,{children:["Not enough history yet. Readings are taken every"," ",a>=60?`${Math.round(a/60)} minutes`:`${a}s`,", and a trend needs about fifteen minutes of them."]});const i=l=>new Date(l.at).toLocaleTimeString();return e.jsx(hs,{data:s,labelOf:l=>i(l),valueOf:l=>n(l),title:l=>`${i(l)}: ${t(n(l))}`})}function Ti({report:s}){const n=s.categories??[],t=s.components??[],a=s.groups??[];return e.jsxs(e.Fragment,{children:[e.jsxs("p",{className:"hint",children:[w(s.total)," goroutines, collected ",F(s.at)," in"," ",s.collectedInMs.toFixed(1)," ms."]}),e.jsxs(xe,{cols:"2",children:[e.jsxs("div",{children:[e.jsx(xs,{children:"By what they are doing"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"State"}),e.jsx("th",{className:"num",children:"Count"})]})}),e.jsx("tbody",{children:n.map(i=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:i.label}),e.jsx("p",{className:"hint",children:i.description}),e.jsx("p",{className:"hint",children:(i.states??[]).map(l=>`${l.state} (${l.count})`).join(" · ")})]}),e.jsx("td",{className:"num",children:w(i.count)})]},i.category))})]})})]}),e.jsxs("div",{children:[e.jsx(xs,{children:"By which part of Memby"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Area"}),e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:t.map(i=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(N,{tone:Ys(i.component),children:i.component})}),e.jsx("td",{className:"num",children:w(i.count)}),e.jsx("td",{className:"num",children:i.longestWaitMinutes>0?`${w(i.longestWaitMinutes)} min`:"—"})]},i.component))})]})})]})]}),e.jsx(xs,{aside:s.groupsTotal>a.length?`the ${a.length} largest of ${w(s.groupsTotal)}`:void 0,children:"Where they are waiting"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Function"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(Y,{columns:5,children:"Nothing to group."}):a.map((i,l)=>e.jsxs("tr",{children:[e.jsx("td",{className:"num",children:w(i.count)}),e.jsx("td",{children:e.jsx(N,{tone:Ys(i.component),children:i.component})}),e.jsx("td",{children:i.state}),e.jsxs("td",{children:[e.jsx("span",{className:"mono",children:i.function}),e.jsx("p",{className:"hint",children:i.file})]}),e.jsx("td",{className:"num",children:i.longestWaitMinutes>0?`${w(i.longestWaitMinutes)} min`:"—"})]},`${i.component}-${i.function}-${i.state}-${l}`))})]})})]})}const Ai=-1;function qe(s){return s===0?"":s<0?"off":String(s)}function De(s,n){const t=s.trim().toLowerCase();if(t==="")return 0;if(n&&(t==="off"||t==="none"||t==="0"))return Ai;const a=Number.parseInt(t,10);return Number.isFinite(a)?a:0}function Fe(s,n){return s<=0?"off":`${s} ${n}${s===1?"":"s"}`}function gs(s){return{timezone:s.timezone??"",logLevel:s.logLevel??"",sessionIdleDays:qe(s.sessionIdleDays),sonarrAlertMinutes:qe(s.sonarrAlertMinutes),radarrAlertMinutes:qe(s.radarrAlertMinutes),embyHealthSeconds:qe(s.embyHealthSeconds),slowRequestMillis:qe(s.slowRequestMillis),librarySyncMinutes:qe(s.librarySyncMinutes)}}function Ei(){const{data:s,error:n,loading:t,reload:a}=J("/admin/api/gateway-settings"),{wrap:i}=ae(),{busy:l,run:r}=ne(),[d,c]=g.useState(null);g.useEffect(()=>{!d&&s&&c(gs(s.settings))},[s,d]);const o=(h,j)=>c(y=>y&&{...y,[h]:j}),u=()=>r("save",async()=>{if(!d)return;const h={timezone:d.timezone.trim(),logLevel:d.logLevel.trim(),sessionIdleDays:De(d.sessionIdleDays,!1),sonarrAlertMinutes:De(d.sonarrAlertMinutes,!0),radarrAlertMinutes:De(d.radarrAlertMinutes,!0),embyHealthSeconds:De(d.embyHealthSeconds,!0),slowRequestMillis:De(d.slowRequestMillis,!0),librarySyncMinutes:De(d.librarySyncMinutes,!0)},j=await i(()=>D.post("/admin/api/gateway-settings",h),"Gateway settings saved.");j&&c(gs(j.settings)),await a()}),p=()=>r("clear",async()=>{const h=await i(()=>D.post("/admin/api/gateway-settings",{timezone:"",logLevel:"",sessionIdleDays:0,sonarrAlertMinutes:0,radarrAlertMinutes:0,embyHealthSeconds:0,slowRequestMillis:0,librarySyncMinutes:0}),"Every setting is back to what this container was deployed with.");h&&c(gs(h.settings)),await a()}),m=s==null?void 0:s.deployed,x=s==null?void 0:s.effective,f=(s==null?void 0:s.logLevels)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Gateway settings",intro:"Server-level settings for this gateway, changeable without a redeployment."}),e.jsx(_,{message:n}),t||!d||!m||!x?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"This gateway",intro:"What the process is running and what it currently believes.",icon:"chip",tone:"info",actions:e.jsx(N,{tone:"info",children:(s==null?void 0:s.version)??"unknown"}),children:e.jsx(We,{rows:[{label:"Server timezone",value:x.timezone||"not set"},{label:"Log level",value:x.logLevel},{label:"Sign-in expiry",value:Fe(x.sessionIdleDays,"day")},{label:"Emby health probe",value:Fe(x.embyHealthSeconds,"second")},{label:"Slow-request breakdown",value:Fe(x.slowRequestMillis,"millisecond")},{label:"Catalogue sweep",value:Fe(x.librarySyncMinutes,"minute")},{label:"Episode alert window",value:Fe(x.sonarrAlertMinutes,"minute")},{label:"Film alert window",value:Fe(x.radarrAlertMinutes,"minute")}]})}),e.jsxs(M,{title:"Overrides",intro:"Leave a field empty to use the value this container was deployed with, shown beneath it. Changes take effect immediately — nothing here needs a restart.",icon:"sliders",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>void u(),children:"Save settings"}),e.jsx($,{busy:l==="clear",onClick:()=>void p(),children:"Use deployed values"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Server timezone",hint:`Deployed: ${m.timezone||"not set"}. An IANA name, for example Pacific/Auckland. Decides what "today" means for the schedule rows, the home hero and the sign-in history.`,children:e.jsx("input",{type:"text",value:d.timezone,placeholder:m.timezone,onChange:h=>o("timezone",h.target.value)})}),e.jsx(q,{label:"Log level",hint:`Deployed: ${m.logLevel}. Applies to the running process at once, so debug can be turned on to watch something happen.`,children:e.jsxs("select",{value:d.logLevel,onChange:h=>o("logLevel",h.target.value),children:[e.jsxs("option",{value:"",children:["Deployed (",m.logLevel,")"]}),f.map(h=>e.jsx("option",{value:h,children:h},h))]})})]}),e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Sign a television out after (days)",hint:`Deployed: ${m.sessionIdleDays} days. A session row holds a live Emby token, so this is how long a set nobody uses keeps working credentials.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.sessionIdleDays,placeholder:String(m.sessionIdleDays),onChange:h=>o("sessionIdleDays",h.target.value)})}),e.jsx(q,{label:"Slow-request breakdown above (ms)",hint:`Deployed: ${m.slowRequestMillis||"off"}. A request slower than this logs where its time went — Emby, Postgres, Redis, row assembly. Lower it while chasing one slow screen; type off to stop annotating altogether.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.slowRequestMillis,placeholder:String(m.slowRequestMillis),onChange:h=>o("slowRequestMillis",h.target.value)})}),e.jsx(q,{label:"Emby health probe (seconds)",hint:`Deployed: ${m.embyHealthSeconds||"off"}. How often the gateway asks Emby whether it is answering. Type off to stop probing, which also removes the outage bar from every television.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.embyHealthSeconds,placeholder:String(m.embyHealthSeconds),onChange:h=>o("embyHealthSeconds",h.target.value)})})]}),e.jsx("div",{className:"fields",children:e.jsx(q,{label:"Catalogue sweep (minutes)",hint:`Deployed: ${m.librarySyncMinutes||"off"}. How often the gateway asks Emby what has changed. With the Sonarr and Radarr webhooks wired up a new file is in the catalogue within a minute of landing, and this is only reconciliation for media they do not manage — 360 is a sensible choice then. Without them it is the only way anything is found.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.librarySyncMinutes,placeholder:String(m.librarySyncMinutes),onChange:h=>o("librarySyncMinutes",h.target.value)})})}),e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Episode alert window (minutes)",hint:`Deployed: ${m.sonarrAlertMinutes||"off"}. How long a "just aired" notice stays on offer to a set that was switched off at the time. Type off to stop announcing them.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.sonarrAlertMinutes,placeholder:String(m.sonarrAlertMinutes),onChange:h=>o("sonarrAlertMinutes",h.target.value)})}),e.jsx(q,{label:"Film alert window (minutes)",hint:`Deployed: ${m.radarrAlertMinutes||"off"}. The same, for a film Radarr has just imported.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.radarrAlertMinutes,placeholder:String(m.radarrAlertMinutes),onChange:h=>o("radarrAlertMinutes",h.target.value)})})]}),e.jsxs(pe,{tone:"note",children:["These override the deployed configuration in the database, so they survive a restart — but a deployment rewrites ",e.jsx("code",{children:".env"}),", not this, and the two can then disagree. Anything meant to be permanent belongs in ",e.jsx("code",{children:".env.example"})," ","as well."]}),s!=null&&s.settings.updatedBy?e.jsxs(pe,{children:["Last changed by ",s.settings.updatedBy,s.settings.updatedAt?` on ${new Date(s.settings.updatedAt).toLocaleString("en-NZ")}`:"","."]}):null]})]})]})}const Ii={done:"ok",pending:"warn",failed:"bad"};function Li(s){const n=s.payload??{};if(n.series){const t=n.episode&&n.episode>0?` S${String(n.season??0).padStart(2,"0")}E${String(n.episode).padStart(2,"0")}`:"";return`${n.series}${t}`}return n.title?n.year?`${n.title} (${n.year})`:n.title:s.key}function qi(){const{data:s,error:n,loading:t}=J("/admin/api/ingest",{pollMs:15e3});if(t)return e.jsx(V,{rows:1});const a=!!(s!=null&&s.sonarrConfigured||s!=null&&s.radarrConfigured),i=(s==null?void 0:s.recent)??[];return e.jsxs(e.Fragment,{children:[e.jsx(_,{message:n}),e.jsxs(M,{title:"Webhook activity",intro:"Sonarr and Radarr are what put files on disk, so they are what the catalogue learns from. A notification is recorded the moment it arrives and read into the catalogue once the file has settled — which is why an import appears here before it appears in Emby.",icon:"plug",tone:"info",children:[e.jsx(ie,{tiles:[{label:"Sonarr webhook",value:s!=null&&s.sonarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.sonarrConfigured?"ok":"warn",icon:"tv"},{label:"Radarr webhook",value:s!=null&&s.radarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.radarrConfigured?"ok":"warn",icon:"play"},{label:"Waiting",value:w((s==null?void 0:s.counts.pending)??0),icon:"clock",tone:"note"},{label:"Given up on",value:w((s==null?void 0:s.counts.failed)??0),icon:"alert",tone:"bad"},{label:"Settle delay",value:`${(s==null?void 0:s.settleSeconds)??0}s`,small:!0,icon:"history",tone:"data"}]}),a?null:e.jsxs("p",{className:"muted",children:["Neither hook has a token, so both answer 404 and nothing is recorded here. Set MEMBY_SONARR_WEBHOOK_TOKEN and MEMBY_RADARR_WEBHOOK_TOKEN, then point each *arr at"," ",e.jsx("code",{children:"/hooks/sonarr"})," and ",e.jsx("code",{children:"/hooks/radarr"}),". Until then the catalogue sweep below is the only way a new title is found."]}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"What"}),e.jsx("th",{children:"Why"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Tries"}),e.jsx("th",{children:"Notes"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(Y,{columns:8,children:a?"Nothing has been imported, upgraded, renamed or deleted since this was switched on.":"No webhook is configured."}):i.map(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(l.updatedAt)}),e.jsx("td",{className:"muted",children:l.source||"—"}),e.jsx("td",{children:Li(l)}),e.jsx("td",{className:"muted",children:l.reason}),e.jsx("td",{children:e.jsx(N,{tone:Ii[l.state]??"warn",children:l.state})}),e.jsx("td",{className:"muted",children:l.outcome||"—"}),e.jsx("td",{className:"num",children:l.attempts}),e.jsx("td",{className:"muted",children:l.lastError||""})]},l.key))})]})})]})]})}function Di(){const{status:s,error:n,loading:t}=je(),a=(s==null?void 0:s.runs)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Imports",intro:"What Sonarr and Radarr said changed, and the catalogue sweep that reconciles everything they do not manage."}),e.jsx(_,{message:n}),e.jsx(qi,{}),t?e.jsx(V,{rows:1}):e.jsx(M,{title:"Synchronisation history",intro:"A full import mark-and-sweeps the catalogue; an incremental one asks Emby for what changed, with a minute of overlap so nothing falls between two runs. With both webhooks wired up this is reconciliation — media dropped in by hand, a title edited in Emby, a notification that never arrived — rather than how new titles are found.",icon:"sync",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Started"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"num",children:"Seen"}),e.jsx("th",{className:"num",children:"Written"}),e.jsx("th",{className:"num",children:"Removed"}),e.jsx("th",{children:"Notes"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(Y,{columns:8,children:"Nothing has been imported yet."}):a.map(i=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(i.startedAt)}),e.jsx("td",{children:i.kind}),e.jsx("td",{className:"muted",children:i.trigger}),e.jsx("td",{children:e.jsx(N,{tone:i.status==="success"?"ok":i.status==="running"?"warn":"bad",children:i.status})}),e.jsx("td",{className:"num",children:w(i.itemsSeen)}),e.jsx("td",{className:"num",children:w(i.itemsUpserted)}),e.jsx("td",{className:"num",children:w(i.itemsRemoved)}),e.jsx("td",{className:"muted",children:i.error||""})]},i.id||i.startedAt))})]})})})]})}const Fi={gateway:"quiet",auth:"note",playback:"info",media:"info",emby:"data",library:"data",subtitles:"data",search:"note",tracearr:"idle",credits:"idle",integrations:"note",requests:"info",home:"quiet"},En=s=>Fi[s]??"quiet",Oi={admin:["gateway","Gateway","Admin"],installer:["gateway","Gateway","Installer"],api:["gateway","Gateway","API"],health:["gateway","Gateway","Health"],status:["gateway","Gateway","Status"],maintenance:["gateway","Gateway","Maintenance"],"quiet-time":["gateway","Gateway","Quiet time"],webhooks:["gateway","Gateway","Webhooks"],scheduler:["gateway","Gateway","Scheduler"],settings:["gateway","Gateway","Settings"],updates:["gateway","Gateway","Updates"],analytics:["gateway","Gateway","Analytics"],auth:["auth","Auth","Session"],devices:["auth","Auth","Devices"],playback:["playback","Playback","Session"],screensaver:["media","Media","Screensaver"],artwork:["media","Media","Artwork"],details:["media","Media","Details"],search:["search","Search","Query"],home:["home","Home","Rows"],"my-shows":["home","Home","My shows"],recommendations:["tracearr","Tracearr","Recommendations"],"for-you":["tracearr","Tracearr","For you"],library:["library","Library","Sync"],credits:["credits","Credits","Scanner"],ratings:["media","Media","Ratings"],integrations:["integrations","Integrations","Arr"],requests:["requests","Requests","Media"],"emby-health":["emby","Emby","Health"]},Pi=[[/^emby |emby (reachable|unreachable|health)/,["emby","Emby","API"]],[/subtitle/,["subtitles","Subtitles","Provider"]],[/^sonarr|sonarr /,["integrations","Integrations","Sonarr"]],[/^radarr|radarr /,["integrations","Integrations","Radarr"]],[/^tracearr/,["tracearr","Tracearr","Signals"]],[/^credits/,["credits","Credits","Scanner"]],[/^library sync/,["library","Library","Sync"]],[/^(signed in|signed out|sign-in rejected)/,["auth","Auth","Session"]],[/^device /,["auth","Auth","Devices"]],[/^(playback (requested|started|stopped|progress))/,["playback","Playback","Session"]],[/^(next episode resolved|trailer playback|trickplay)/,["playback","Playback","Player"]],[/^scheduled task/,["gateway","Gateway","Scheduler"]],[/^(update offered|update policy)/,["gateway","Gateway","Updates"]]];function _i(s,n){const t=n.toLowerCase();for(const[i,l]of Pi)if(i.test(t))return l;const a=Oi[s];return a||(s?["gateway","Gateway",ms(s.replace(/[-_]/g," "))]:["gateway","Gateway","Server"])}const Ui={h:36e5,m:6e4,s:1e3,ms:1,us:.001,µs:.001,ns:1e-6};function fs(s){if(typeof s=="number")return Number.isFinite(s)?s:null;if(typeof s!="string"||!s)return null;const n=s.matchAll(/([0-9]*\.?[0-9]+)(ns|µs|us|ms|h|m|s)/g);let t=0,a=!1;for(const i of n){const l=i[2]?Ui[i[2]]:void 0;l!==void 0&&(t+=Number(i[1])*l,a=!0)}return a?t:null}function $s(s){return s<1?"<1 ms":s<1e3?`${Math.round(s)} ms`:s<1e4?`${(s/1e3).toFixed(1)} s`:s<6e4?`${Math.round(s/1e3)} s`:`${Math.floor(s/6e4)}m ${Math.round(s%6e4/1e3)}s`}const Wi=s=>s>=3e3?"bad":s>=1e3?"warn":null,Bi={200:"OK",201:"Created",202:"Accepted",204:"No Content",206:"Partial Content",301:"Moved Permanently",302:"Found",304:"Not Modified",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",409:"Conflict",412:"Precondition Failed",418:"Client Closed Request",426:"Upgrade Required",429:"Too Many Requests",499:"Client Closed Request",500:"Internal Server Error",502:"Bad Gateway",503:"Service Unavailable",504:"Gateway Timeout"};function Vi(s){const n=Bi[s];return n||(s>=500?"Server Error":s>=400?"Client Error":s>=300?"Redirected":s>=200?"OK":"Response")}const Hi=s=>s>=500?"bad":s>=400?"warn":s>=300?"quiet":"ok",zi={"/healthz":"Health probe","/readyz":"Readiness probe","/v1/status":"Status poll","/v1/home":"Home rows","/v1/features":"Features","/v1/preferences":"Preferences","/v1/theme":"Theme","/v1/magic":"Magic pick","/v1/calendar":"TV calendar","/v1/search":"Search","/v1/update":"Update check"},Ki=s=>/\d/.test(s)||s.length>24;function Qs(s){const n=zi[s];if(n)return n;const t=s.split("/").filter(i=>i&&i!=="v1"&&i!=="api");t[0]==="admin"&&t.shift();const a=t.filter(i=>!Ki(i));return a.length===0?s:ms(a.join(" ").replace(/[-_.]/g," ").replace(/\s+/g," ").trim())}const ms=s=>s&&s.charAt(0).toUpperCase()+s.slice(1),es=s=>ms(s.replace(/_/g," ")),ve=s=>s==null?"":String(s),In=new Set(["","unknown","none","null","","0"]),Me=s=>!In.has(ve(s).toLowerCase()),Gi=["title","series","name","query","item_title","file"],Zi={directplay:"ok",direct:"ok",directstream:"ok",transcode:"warn",transcoding:"warn"};function Yi(s,n,t){if(t!==null)return{label:`${t} ${Vi(t)}`,short:String(t),tone:Hi(t)};if(Me(s.error))return{label:"Failed",short:"Failed",tone:n==="WARN"?"warn":"bad"};const a=ve(s.play_method).toLowerCase().replace(/[\s_-]/g,"");if(a&&!In.has(a)){const i=ms(ve(s.play_method).replace(/([a-z])([A-Z])/g,"$1 $2"));return{label:i,short:i,tone:Zi[a]??"info"}}if(Me(s.cache)){const i=/hit|true|yes/i.test(ve(s.cache));return{label:i?"Cached":"Cache miss",short:i?"Cached":"Miss",tone:i?"data":"quiet"}}return n==="ERROR"?{label:"Failed",short:"Failed",tone:"bad"}:n==="WARN"?{label:"Warning",short:"Warning",tone:"warn"}:null}function Ji(s){const n=[];Me(s.user)&&n.push(ve(s.user)),Me(s.device)&&n.push(ve(s.device));const t=fs(s.marker_ms);t!==null&&t>0&&n.push(`Start ${Xs(t)}`);const a=fs(s.position);return a!==null&&a>0&&n.push(`At ${Xs(a)}`),Me(s.watched)&&n.push(`${ve(s.watched)} watched`),Me(s.reason)&&n.push(ve(s.reason)),n.slice(0,3).join(" · ")}function Xs(s){const n=Math.round(s/1e3),t=Math.floor(n/3600),a=Math.floor(n%3600/60),i=n%60,l=r=>String(r).padStart(2,"0");return t>0?`${t}:${l(a)}:${l(i)}`:`${a}:${l(i)}`}const Ln=[{title:"Request",keys:["method","path","query_keys","status","cache","client","protocol","host"]},{title:"Context",keys:["user","user_id","device","device_id","item","title","series","type","play_method","play_session_id","media_source_id","position","resume","runtime","watched","subtitles","subtitle_track","subtitle_language","event_name"]},{title:"Where the time went",keys:["duration","breakdown","elsewhere"]},{title:"Diagnostics",keys:["error","stack","correlation","version","gateway_version"]}],Qi=new Set(Ln.flatMap(s=>s.keys)),Xi=s=>Qi.has(s),ea=new Intl.DateTimeFormat("en-NZ",{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),sa=new Intl.DateTimeFormat("en-NZ",{weekday:"short",day:"numeric",month:"short"}),en=new WeakMap;function Ze(s){const n=en.get(s);if(n)return n;const t=na(s);return en.set(s,t),t}function na(s){const n=s.attributes??{},t=s.message??"",[a,i,l]=_i(ve(n.component),t),r=ve(n.path),d=r?ve(n.method).toUpperCase():"",c=Number(n.status),o=r&&Number.isFinite(c)&&c>0?c:null,u=t==="request"&&!!r,p=Gi.find(L=>Me(n[L])),m=p?ve(n[p]):"";let x,f;u?(x=d||"HTTP",f=Qs(r)):r&&d?(x=d,f=m?`${es(t)} · ${m}`:`${es(t)} — ${Qs(r)}`):(x="",f=m?`${es(t)} · ${m}`:es(t));const h=fs(n.duration??n.duration_ms??n.negotiation_duration),j=Me(n.error)?ve(n.error):"",y=Ji(n),v=new Date(s.occurredAt),b=ve(n.breakdown),T=j||(u?b:y),k=j?"error":T?"context":"",I=!j&&u?y:"",E=Object.entries(n);return{serviceKey:a,service:i,component:l,action:x,summary:f,context:y,detail:j,secondary:T,secondaryTone:k,trail:I,result:Yi(n,s.level,o),durationMs:h,method:d,status:o,eventKey:t,level:s.level,time:`${ea.format(v)}.${String(v.getMilliseconds()).padStart(3,"0")}`,day:sa.format(v),dayKey:v.toDateString(),tall:!!T,haystack:[t,i,l,f,y,j,...E.flat().map(ve)].join(" ").toLowerCase(),fields:E,attributes:n}}const sn={TRACE:5,DEBUG:10,INFO:20,WARN:30,ERROR:40},ss={level:"INFO",service:"",component:"",event:"",method:"",status:"",slower:0,text:""};function ta(s,n){if(!s)return!0;if(n===null)return!1;if(s==="error")return n>=400;const t=Number(s[0]);return Math.floor(n/100)===t}function ia(s,n,t){if((sn[s.level]??0)<(sn[n.level]??20))return!1;const a=Ze(s);return!(n.service&&a.serviceKey!==n.service||n.component&&a.component!==n.component||n.event&&a.eventKey!==n.event||n.method&&a.method!==n.method||!ta(n.status,a.status)||n.slower>0&&(a.durationMs??0)({key:l,label:r})).sort((l,r)=>l.label.localeCompare(r.label)),components:[...t].sort((l,r)=>l.localeCompare(r)),events:[...a].sort((l,r)=>l.localeCompare(r)),methods:[...i].sort((l,r)=>l.localeCompare(r))}}function ra(s,n){var a;const t=[];if(s.service){const i=((a=n.find(l=>l.key===s.service))==null?void 0:a.label)??s.service;t.push({key:"service",label:`Service: ${i}`})}return s.component&&t.push({key:"component",label:`Component: ${s.component}`}),s.event&&t.push({key:"event",label:`Event: ${s.event}`}),s.method&&t.push({key:"method",label:`Method: ${s.method}`}),s.status&&t.push({key:"status",label:`Status: ${s.status==="error"?"≥400":s.status}`}),s.slower>0&&t.push({key:"slower",label:`Duration: >${$s(s.slower)}`}),s.text&&t.push({key:"text",label:`Search: ${s.text}`}),t}const ns=2e4,la=5e3,nn=7,oa=16,ca=15,da=2,ha=1,ys=nn+oa+nn+ha,tn=ys+da+ca,an=26,ua=31,rn=10,ma=[{value:"TRACE",label:"Everything"},{value:"DEBUG",label:"Debug+"},{value:"INFO",label:"Info+"},{value:"WARN",label:"Warnings+"},{value:"ERROR",label:"Errors only"}],pa=[{value:"",label:"Any result"},{value:"2xx",label:"Success (2xx)"},{value:"3xx",label:"Redirect (3xx)"},{value:"4xx",label:"Client error (4xx)"},{value:"5xx",label:"Server error (5xx)"},{value:"error",label:"Failed (≥400)"}],xa=[{value:0,label:"Any duration"},{value:100,label:"Slower than 100 ms"},{value:500,label:"Slower than 500 ms"},{value:1e3,label:"Slower than 1 s"},{value:3e3,label:"Slower than 3 s"}],ln=s=>s.replace(/_/g," ");function ts({onPick:s,className:n,title:t,children:a,...i}){return e.jsx("button",{type:"button",className:`logfacet ${n}`,title:t,onClick:s,...i,children:a})}const ja=g.memo(function({event:n,view:t,top:a,height:i,selected:l,onInspect:r,onFilter:d}){const c=t.durationMs!==null?Wi(t.durationMs):null;return e.jsxs("div",{className:"logrow","data-level":t.level,"data-selected":l||void 0,style:{transform:`translateY(${a}px)`,height:`${i}px`},children:[e.jsx("time",{className:"logrow-time",title:n.occurredAt,children:t.time}),e.jsx(ts,{className:"logrow-level","data-level":t.level,title:`Show ${t.level} and above`,onPick:()=>d({level:t.level}),children:t.level}),e.jsxs("span",{className:"logrow-place",children:[e.jsx(ts,{className:"logrow-service","data-tone":En(t.serviceKey),title:`Filter to ${t.service}`,onPick:()=>d({service:t.serviceKey,component:""}),children:t.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),e.jsx(ts,{className:"logrow-component",title:`Filter to ${t.component}`,onPick:()=>d({component:t.component}),children:t.component})]}),e.jsxs("button",{type:"button",className:"logrow-summary",title:[t.summary,t.trail,t.secondary].filter(Boolean).join(" — "),onClick:()=>r(n.sequence),children:[e.jsxs("span",{className:"logrow-line",children:[t.action?e.jsx("b",{className:"logrow-action","data-method":t.method||void 0,children:t.action}):null,e.jsx("span",{className:"logrow-text",children:t.summary}),t.trail?e.jsx("span",{className:"logrow-trail",children:t.trail}):null]}),t.secondary?e.jsx("span",{className:"logrow-second","data-tone":t.secondaryTone,children:t.secondaryTone==="error"?`↳ ${t.secondary}`:t.secondary}):null]}),e.jsx("span",{className:"logrow-result",children:t.result?e.jsx(ts,{className:"logrow-verdict","data-tone":t.result.tone,title:t.status!==null?`Filter to ${t.status}`:`Filter to ${t.eventKey}`,onPick:()=>t.status!==null?d({status:`${Math.floor(t.status/100)}xx`}):d({event:t.eventKey}),children:t.result.label}):null}),e.jsx("span",{className:"logrow-duration","data-tone":c??void 0,children:t.durationMs!==null?$s(t.durationMs):""})]})});function ga({event:s,view:n,onClose:t}){const[a,i]=g.useState(!1),l=n.fields.filter(([c])=>!Xi(c)&&c!=="component"),r=async()=>{try{await navigator.clipboard.writeText(JSON.stringify(s,null,2)),i(!0),window.setTimeout(()=>i(!1),1600)}catch{i(!1)}},d=Ln.map(c=>({title:c.title,rows:c.keys.map(o=>[o,n.attributes[o]]).filter(([,o])=>o!=null&&String(o)!=="")})).filter(c=>c.rows.length>0);return e.jsxs("section",{className:"logdrawer","aria-label":`Log record ${s.sequence}`,children:[e.jsxs("header",{className:"logdrawer-head",children:[e.jsxs("div",{children:[e.jsxs("p",{className:"logdrawer-place",children:[e.jsx("span",{className:"logrow-service","data-tone":En(n.serviceKey),children:n.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),n.component]}),e.jsx("b",{children:n.summary}),n.detail?e.jsx("p",{className:"logdrawer-error",children:n.detail}):null]}),e.jsxs("div",{className:"logdrawer-actions",children:[e.jsx($,{size:"sm",variant:"quiet",onClick:r,icon:"download",children:a?"Copied":"Copy JSON"}),e.jsx($,{size:"sm",variant:"quiet",onClick:t,icon:"close",children:"Close"})]})]}),e.jsxs("div",{className:"logdrawer-grid",children:[e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Overview"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Time"}),e.jsxs("dd",{children:[n.day," ",n.time]}),e.jsx("dt",{children:"Level"}),e.jsx("dd",{children:n.level}),e.jsx("dt",{children:"Service"}),e.jsxs("dd",{children:[n.service," › ",n.component]}),e.jsx("dt",{children:"Event"}),e.jsx("dd",{children:n.eventKey}),n.result?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Result"}),e.jsx("dd",{children:n.result.label})]}):null,n.durationMs!==null?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Duration"}),e.jsx("dd",{children:$s(n.durationMs)})]}):null,e.jsx("dt",{children:"Record"}),e.jsxs("dd",{children:["#",s.sequence]})]})]}),d.map(c=>e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:c.title}),e.jsx("dl",{children:c.rows.map(([o,u])=>e.jsxs(g.Fragment,{children:[e.jsx("dt",{children:ln(o)}),e.jsx("dd",{children:String(u)})]},o))})]},c.title)),l.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Details"}),e.jsx("dl",{children:l.map(([c,o])=>e.jsxs(g.Fragment,{children:[e.jsx("dt",{children:ln(c)}),e.jsx("dd",{children:String(o)})]},c))})]}):null]}),e.jsxs("details",{className:"logdrawer-raw",children:[e.jsx("summary",{children:"Raw event"}),e.jsx("pre",{children:JSON.stringify(s,null,2)})]})]})}function va(){var He;const[s,n]=g.useState([]),[t,a]=g.useState(0),[i,l]=g.useState(!1),[r,d]=g.useState(0),[c,o]=g.useState(()=>{const C=new URLSearchParams(window.location.search).get("q")??"";return C?{...ss,text:C}:ss}),[u,p]=g.useState(""),[m,x]=g.useState({top:0,height:600}),[f,h]=g.useState(!0),[j,y]=g.useState(null),v=g.useDeferredValue(c.text.trim().toLowerCase()),b=g.useRef(0),T=g.useRef(!1),k=g.useRef(0),I=g.useRef(null),E=g.useRef(!0),A=g.useRef(void 0),L=g.useRef([]),X=g.useRef(i);g.useEffect(()=>{X.current=i},[i]);const le=g.useCallback(C=>{n(B=>{const Q=B.concat(C);return Q.length>ns?Q.slice(Q.length-ns):Q})},[]),ue=g.useCallback(async()=>{if(T.current||document.hidden)return;T.current=!0;const C=k.current,B=[];let Q=0;try{let fe=0,Ce;do Ce=await D.get(`/admin/api/events?after=${b.current}&limit=1000`),b.current=Ce.next||b.current,Q+=Ce.dropped||0,B.push(...Ce.events??[]),fe+=1;while(Ce.hasMore&&fe<20);p("")}catch(fe){p(fe instanceof Error?fe.message:String(fe))}finally{B.length>0&&C===k.current&&(X.current?(L.current=L.current.concat(B),L.current.length>ns&&(L.current=L.current.slice(L.current.length-ns)),d(L.current.length)):le(B)),Q>0&&C===k.current&&a(fe=>fe+Q),T.current=!1}},[le]);g.useEffect(()=>{let C;const B=()=>{window.clearInterval(C),C=document.hidden?void 0:window.setInterval(()=>void ue(),la)},Q=()=>{B(),document.hidden||ue()};return ue(),B(),document.addEventListener("visibilitychange",Q),()=>{window.clearInterval(C),document.removeEventListener("visibilitychange",Q)}},[ue]);const te=g.useCallback(()=>{X.current=!0,l(!0)},[]),H=g.useCallback(()=>{X.current=!1;const C=L.current;L.current=[],d(0),l(!1),C.length&&le(C)},[le]),re=g.useCallback(C=>{o(B=>({...B,...C}))},[]),S=g.useMemo(()=>s.filter(C=>ia(C,c,v)),[s,c,v]),O=g.useMemo(()=>{const C=new Float64Array(S.length+1),B=new Uint8Array(S.length),Q=new Uint8Array(S.length);let fe=0,Ce="";for(let Le=0;Le{let C=0,B=S.length;for(;C>1;(O.tops[Q]??0)+(O.heights[Q]??0)<=z?C=Q+1:B=Q}return Math.max(0,C-rn)},[O,z,S.length]),we=g.useMemo(()=>{const C=z+m.height;let B=oe;for(;B{const C=[];for(let B=oe;Bs.find(C=>C.sequence===j),[s,j]),R=g.useCallback(()=>{const C=I.current;C&&(E.current=!0,C.scrollTop=C.scrollHeight,h(!0),x({top:C.scrollTop,height:C.clientHeight}))},[]);g.useLayoutEffect(()=>{const C=I.current;!C||!E.current||(C.scrollTop=C.scrollHeight,x({top:C.scrollTop,height:C.clientHeight}))},[Te,O.total]),g.useEffect(()=>()=>window.cancelAnimationFrame(A.current??0),[]);const P=()=>{const C=I.current;if(!C)return;const B=C.scrollHeight-C.scrollTop-C.clientHeight{x({top:C.scrollTop,height:C.clientHeight})})},de=()=>{const C=new Blob([JSON.stringify(S,null,2)],{type:"application/json"}),B=document.createElement("a");B.href=URL.createObjectURL(C),B.download=`memby-events-${new Date().toISOString().replace(/[:.]/g,"-")}.json`,B.click(),window.setTimeout(()=>URL.revokeObjectURL(B.href),1e3)},me=g.useMemo(()=>aa(s),[s]),Ie=ra(c,me.services);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Server logs",intro:"Structured gateway events as they happen."}),e.jsx(_,{message:u}),e.jsxs(M,{children:[e.jsxs("div",{className:"logbar",children:[e.jsxs("div",{className:"logbar-filters",children:[e.jsxs("select",{"aria-label":"Service",value:c.service,onChange:C=>re({service:C.target.value,component:""}),children:[e.jsx("option",{value:"",children:"All services"}),me.services.map(C=>e.jsx("option",{value:C.key,children:C.label},C.key))]}),e.jsxs("select",{"aria-label":"Component",value:c.component,onChange:C=>re({component:C.target.value}),children:[e.jsx("option",{value:"",children:"All components"}),me.components.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Level",value:c.level,onChange:C=>re({level:C.target.value}),children:ma.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Event",value:c.event,onChange:C=>re({event:C.target.value}),children:[e.jsx("option",{value:"",children:"All events"}),me.events.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Result",value:c.status,onChange:C=>re({status:C.target.value}),children:pa.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Method",value:c.method,onChange:C=>re({method:C.target.value}),children:[e.jsx("option",{value:"",children:"Any method"}),me.methods.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Duration",value:String(c.slower),onChange:C=>re({slower:Number(C.target.value)}),children:xa.map(C=>e.jsx("option",{value:String(C.value),children:C.label},C.value))}),e.jsxs("label",{className:"logsearch",children:[e.jsx(se,{name:"search"}),e.jsx("input",{type:"search",value:c.text,"aria-label":"Search logs",placeholder:"Search person, title, service, component, path, request ID…",onChange:C=>re({text:C.target.value})})]})]}),e.jsxs("div",{className:"logbar-actions",children:[e.jsx($,{size:"sm",variant:"quiet",onClick:()=>i?H():te(),icon:i?"play":"clock",children:i?r?`Resume (${w(r)})`:"Resume":"Pause"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{k.current+=1,L.current=[],d(0),n([]),a(0),y(null)},children:"Clear view"}),e.jsx($,{size:"sm",variant:"quiet",onClick:de,icon:"download",title:"Export the rows matching the current filters, as delivered by the gateway",children:"Export JSON"})]})]}),Ie.length?e.jsxs("div",{className:"logchips",children:[Ie.map(C=>e.jsxs("button",{type:"button",className:"logchip",onClick:()=>re({[C.key]:ss[C.key]}),children:[C.label,e.jsx(se,{name:"close"})]},C.key)),e.jsx("button",{type:"button",className:"logchip logchip-clear",onClick:()=>o(ss),children:"Clear all"})]}):null,e.jsxs("div",{className:"logshell",children:[e.jsxs("div",{className:"logview",ref:I,onScroll:P,role:"log","aria-label":"Server events",children:[e.jsxs("div",{className:"loghead","aria-hidden":"true",children:[e.jsx("span",{children:"Time"}),e.jsx("span",{children:"Level"}),e.jsx("span",{children:"Service"}),e.jsx("span",{children:"Event"}),e.jsx("span",{children:"Result"}),e.jsx("span",{children:"Duration"})]}),S.length===0?e.jsx("p",{className:"empty",children:s.length===0?"Waiting for server events…":"No events match these filters."}):e.jsx("div",{className:"logbody",style:{height:`${O.total}px`},children:Re.map(({event:C,view:B,index:Q})=>e.jsxs(g.Fragment,{children:[O.divider[Q]?e.jsx("div",{className:"logday",style:{transform:`translateY(${(O.tops[Q]??0)-an}px)`},children:e.jsx("span",{children:B.day})}):null,e.jsx(ja,{event:C,view:B,top:O.tops[Q]??0,height:O.heights[Q]??ys,selected:C.sequence===j,onInspect:y,onFilter:re})]},C.sequence))})]}),!f&&S.length>0?e.jsxs("button",{type:"button",className:"logtail",onClick:R,children:[e.jsx(se,{name:"caret"}),"Jump to latest"]}):null]}),e.jsxs("p",{className:"hint",children:[w(s.length)," retained · ",w(S.length)," matching",S.length?` · ${w(Re.length)} rows mounted`:"",t?` · ${w(t)} overwritten before delivery`:"",i?` · paused${r?`, ${w(r)} held`:""}`:""]}),ge?e.jsx(ga,{event:ge,view:Ze(ge),onClose:()=>y(null)}):s.length?e.jsx(pe,{children:"Select a row to see the full record — request, context, diagnostics and raw event."}):null]})]})}const on=["home","movies","shows","favorites","search","recent_searches","genre_browse","for_you","for_you_time","recommendation","continue","latest","my_shows","details","playback","magic_movie","notifications","profiles","settings"];function Ae(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function ba(){const[s,n]=g.useState(30),[t,a]=g.useState(""),i=ws(),l=g.useMemo(()=>`/admin/api/journeys${Ve({days:s,userId:t})}`,[s,t]),{data:r,error:d,loading:c}=J(l),o=r==null?void 0:r.stats,u=(r==null?void 0:r.users)??[],p=(r==null?void 0:r.actions)??[],m=(r==null?void 0:r.paths)??[],x=m[0],f=g.useMemo(()=>{const h=new Map(((r==null?void 0:r.features)??[]).map(y=>[y.feature,y])),j=new Map(on.map((y,v)=>[y,v]));return[...new Set([...on,...h.keys()])].map(y=>({name:y,stat:h.get(y)})).sort((y,v)=>{var T,k;const b=(((T=v.stat)==null?void 0:T.uses)??0)-(((k=y.stat)==null?void 0:k.uses)??0);return b||(j.get(y.name)??Number.MAX_SAFE_INTEGER)-(j.get(v.name)??Number.MAX_SAFE_INTEGER)})},[r==null?void 0:r.features]);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User journeys",intro:"How viewers move through Memby, use features and complete flows."}),e.jsx(_,{message:d}),e.jsx(M,{title:"Journey health",intro:"Server-derived foreground visits, completion and interruption. Search text, content titles and setting values are never stored.",icon:"people",tone:"info",actions:e.jsxs(e.Fragment,{children:[e.jsx(q,{label:"Window",children:e.jsxs("select",{value:s,onChange:h=>n(Number(h.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"}),e.jsx("option",{value:90,children:"90 days"})]})}),e.jsx(q,{label:"User",children:e.jsxs("select",{value:t,onChange:h=>{const j=h.target.value;a(j),j&&i(`/admin/journeys/${encodeURIComponent(j)}`)},children:[e.jsx("option",{value:"",children:"All users"}),u.map(h=>e.jsx("option",{value:h.userId,children:h.username||h.userId},h.userId))]})})]}),children:c?e.jsx(V,{rows:1}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:"journeys",value:w(o==null?void 0:o.journeys),icon:"list",tone:"data"},{label:"viewers",value:w(o==null?void 0:o.viewers),icon:"people",tone:"info"},{label:"completion",value:as(o==null?void 0:o.completionRate),icon:"check",tone:"ok"},{label:"abandoned",value:w(o==null?void 0:o.abandoned),icon:"alert",tone:"note"},{label:"active now",value:w(o==null?void 0:o.active),icon:"pulse",tone:"info"},{label:"average steps",value:((o==null?void 0:o.averageSteps)??0).toFixed(1),icon:"chart"},{label:"average visit",value:ye(o==null?void 0:o.averageTimeMs),small:!0,icon:"clock"},{label:"history kept",value:`${(r==null?void 0:r.retentionDays)??90} days`,small:!0,icon:"clock"}]}),e.jsxs("div",{className:"summary-grid",children:[e.jsxs("div",{className:"summary",children:[e.jsxs("div",{className:"summary-head",children:[e.jsx("b",{children:"Visit completion"}),e.jsx("strong",{children:as(o==null?void 0:o.completionRate)})]}),e.jsx(Mn,{value:(o==null?void 0:o.completed)??0,total:(o==null?void 0:o.journeys)??0}),e.jsxs("p",{children:[w(o==null?void 0:o.completed)," completed · ",w(o==null?void 0:o.abandoned)," abandoned ·"," ",w(o==null?void 0:o.active)," active"]})]}),e.jsxs("div",{className:"summary",children:[e.jsx("div",{className:"summary-head",children:e.jsx("b",{children:"Most common route"})}),e.jsx("strong",{style:void 0,children:x?`${Ae(x.from)} → ${Ae(x.to)}`:"Not enough data"}),e.jsx("p",{children:x?`${w(x.count)} times in this window`:"Journeys will appear here as viewers move through Memby."})]})]})]})}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"What people do",intro:"Actions show total use and how many separate visits included them.",icon:"chart",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Action"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{className:"num",children:"Visits"})]})}),e.jsx("tbody",{children:p.length===0?e.jsx(Y,{columns:3,children:"No significant actions in this window."}):p.map(h=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:Ae(h.action)}),e.jsx("span",{className:"table-sub",children:Ae(h.category)})]}),e.jsx("td",{className:"num",children:w(h.events)}),e.jsx("td",{className:"num",children:w(h.journeys)})]},`${h.category}:${h.action}`))})]})})}),e.jsx(M,{title:"Where people go",intro:"The most common steps between screens, including where quiet visits ended.",icon:"list",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Route"}),e.jsx("th",{className:"num",children:"Times"})]})}),e.jsx("tbody",{children:m.length===0?e.jsx(Y,{columns:2,children:"No repeated paths in this window."}):m.map((h,j)=>e.jsxs("tr",{children:[e.jsxs("td",{children:[Ae(h.from)," ",e.jsx("span",{className:"route-arrow",children:"→"})," ",Ae(h.to)]}),e.jsx("td",{className:"num",children:w(h.count)})]},`${h.from}:${h.to}:${j}`))})]})})})]}),e.jsx(M,{title:"Feature use",intro:"Rare and unused features are shown against Memby's major feature catalogue.",icon:"pulse",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Feature"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{children:"Last used"}),e.jsx("th",{children:"Status"})]})}),e.jsx("tbody",{children:f.map(({name:h,stat:j})=>{const y=(j==null?void 0:j.uses)??0;return e.jsxs("tr",{children:[e.jsx("td",{children:Ae(h)}),e.jsx("td",{className:"num",children:w(y)}),e.jsx("td",{className:"muted nowrap",children:j?F(j.lastUsedAt):"—"}),e.jsx("td",{children:y===0?e.jsx(N,{tone:"warn",children:"not used"}):y<3?e.jsx(N,{tone:"note",children:"rare"}):e.jsx(N,{tone:"ok",children:"used"})})]},h)})})]})})}),t?null:e.jsx(M,{title:"Inspect a viewer",intro:"Choose a person above to open their dedicated session and viewing-journey timeline.",icon:"journey",tone:"info",children:e.jsx("p",{className:"empty",children:"A viewing journey follows one intent through to playback, so two films watched in a single app session appear as two separate journeys."})})]})}const $e=s=>{const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n},qn=s=>$e((s==null?void 0:s.target)||(s==null?void 0:s.screen)||(s==null?void 0:s.source)||(s==null?void 0:s.feature)),fa=s=>{const n=s.find(t=>t.category==="playback"&&t.action==="request");return n!=null&&n.source?$e(n.source):qn(s[0])},cn=s=>s.itemName?`${$e(s.itemType)} · ${s.itemName}`:s.source&&s.target?`${$e(s.source)} → ${$e(s.target)}`:qn(s),ya=s=>({journey_start:"Opened Memby",home_open:"Opened Memby",journey_end:"Finished session",screen_view:"Viewed",select:"Selected",open:"Opened",close:"Closed",request:s.category==="playback"?"Asked to watch":"Requested",stop:"Left the player",start:s.category==="playback"?s.outcome==="failure"?"Playback failed":"Started watching":"Started",complete:s.category==="playback"?s.outcome==="completed"?"Finished watching":"Stopped watching":"Completed"})[s.action]??$e(s.action);function wa(s){var a;const n=[...s].reverse().find(i=>i.category==="playback"&&i.outcome);if((n==null?void 0:n.outcome)==="failure")return{label:"playback failed",tone:"warn"};if((n==null?void 0:n.outcome)==="completed")return{label:"watched",tone:"ok"};if((n==null?void 0:n.outcome)==="abandoned")return{label:"stopped part-way",tone:"note"};if((n==null?void 0:n.outcome)==="success")return{label:"watched",tone:"ok"};const t=(a=[...s].reverse().find(i=>i.outcome))==null?void 0:a.outcome;return t==="success"||t==="completed"?{label:$e(t),tone:"ok"}:t==="failure"||t==="cancelled"||t==="abandoned"?{label:$e(t),tone:"note"}:s.some(i=>i.action==="stop"&&i.category==="playback")?{label:"watched",tone:"ok"}:{label:"left before playback ended",tone:"warn"}}function ka(s){const n=s.reduce((t,a,i)=>(a.category==="playback"&&a.action==="request"&&t.push(i),t),[]);return n.length===0?[s]:n.map((t,a)=>s.slice(a===0?0:t,n[a+1]??s.length))}function Na(){var c,o;const{userId:s=""}=Be(),n=g.useMemo(()=>`/admin/api/journeys${Ve({days:90,userId:s})}`,[s]),{data:t,error:a,loading:i}=J(n),l=((o=(c=t==null?void 0:t.users)==null?void 0:c.find(u=>u.userId===s))==null?void 0:o.username)||s,r=g.useMemo(()=>{const u=new Map;for(const p of(t==null?void 0:t.events)??[])u.set(p.journeyId,[...u.get(p.journeyId)??[],p]);return[...u.values()].map(p=>p.sort((m,x)=>m.sequence-x.sequence)).sort((p,m)=>{var x,f;return(((x=m[0])==null?void 0:x.occurredAt)??"").localeCompare(((f=p[0])==null?void 0:f.occurredAt)??"")})},[t==null?void 0:t.events]),d=r.flatMap(u=>ka(u).map((p,m)=>{var x;return{events:p,key:`${(x=u[0])==null?void 0:x.journeyId}:${m}`}}));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:`${l}'s journeys`,intro:"Each app session is shown as the viewing journeys it contains: entry, selection, playback outcome.",icon:"journey",crumbs:e.jsx(Z,{className:"crumb",to:"/admin/journeys",children:"Journeys"})}),e.jsx(_,{message:a}),i?e.jsx(V,{}):e.jsx(M,{title:"Viewing journeys",intro:`${r.length} app session${r.length===1?"":"s"} · ${d.length} viewing journey${d.length===1?"":"s"} in the last 90 days.`,icon:"journey",tone:"info",children:e.jsx("div",{className:"visits",children:d.length===0?e.jsx("p",{className:"empty",children:"No journeys recorded for this viewer."}):d.map((u,p)=>{const m=u.events,x=m[0],f=[...m].reverse().find(j=>j.itemName||j.action==="select"||j.category==="playback"&&j.action==="request"),h=wa(m);return e.jsxs("article",{className:"visit",children:[e.jsxs("header",{children:[e.jsxs("div",{children:[e.jsx("b",{children:F(x==null?void 0:x.occurredAt)}),e.jsxs("span",{children:["Journey ",p+1," · ",m.length," recorded steps"]})]}),e.jsx(N,{tone:h.tone,children:h.label})]}),e.jsxs("div",{className:"journey-answers",children:[e.jsxs("div",{className:"journey-answer","data-kind":"entry",children:[e.jsx(se,{name:"journey"}),e.jsx("span",{children:"Entered from"}),e.jsx("b",{children:fa(m)})]}),e.jsxs("div",{className:"journey-answer","data-kind":"selection",children:[e.jsx(se,{name:"play"}),e.jsx("span",{children:"Selected"}),e.jsx("b",{children:f?cn(f):"Nothing selected"})]}),e.jsxs("div",{className:"journey-answer","data-kind":"outcome",children:[e.jsx(se,{name:h.tone==="ok"?"check":"clock"}),e.jsx("span",{children:"Outcome"}),e.jsx("b",{children:h.label})]})]}),e.jsx("ol",{className:"journey-timeline",children:m.map(j=>e.jsxs("li",{children:[e.jsx("span",{className:"timeline-dot","data-action":j.action}),e.jsxs("div",{children:[e.jsx("b",{children:ya(j)}),e.jsx("span",{children:cn(j)})]}),e.jsx("time",{children:F(j.occurredAt)})]},`${j.journeyId}:${j.sequence}`))})]},u.key)})})})]})}function dn(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function Sa(){const[s,n]=g.useState(30),{data:t,error:a,loading:i}=J(`/admin/api/analytics?days=${s}`),l=(t==null?void 0:t.rows)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row."}),e.jsx(_,{message:a}),e.jsx(M,{title:"Launcher rows",intro:"Impressions are rows drawn, focuses are rows the D-pad reached, and dwell is how long it stayed there. Open rate is what a row was worth.",icon:"chart",tone:"info",actions:e.jsx(q,{label:"Window",children:e.jsxs("select",{value:s,onChange:r=>n(Number(r.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"}),e.jsx("option",{value:90,children:"90 days"})]})}),children:i?e.jsx(V,{rows:1}):e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Row"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{className:"num",children:"Dwell"}),e.jsx("th",{className:"num",children:"Impressions"}),e.jsx("th",{className:"num",children:"Focuses"}),e.jsx("th",{className:"num",children:"Opened"}),e.jsx("th",{className:"num",children:"Open rate"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:l.length===0?e.jsx(Y,{columns:8,children:"No events in this window."}):l.map(r=>e.jsxs("tr",{children:[e.jsx("td",{children:dn(r.rowId)}),e.jsx("td",{className:"muted",children:dn(r.rowKind)}),e.jsx("td",{className:"num",children:ye(r.dwellMs)}),e.jsx("td",{className:"num",children:w(r.impressions)}),e.jsx("td",{className:"num",children:w(r.focuses)}),e.jsx("td",{className:"num",children:w(r.selects)}),e.jsx("td",{className:"num",children:as(r.selectRate)}),e.jsx("td",{className:"num",children:w(r.viewers)})]},`${r.rowId}:${r.rowKind}`))})]})})})]})}function Ca(){const[s,n]=g.useState(7),{data:t,error:a,loading:i}=J(`/admin/api/searches?days=${s}`),l=(t==null?void 0:t.terms)??[],r=(t==null?void 0:t.recent)??[],d=t==null?void 0:t.totals;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Searches",intro:"What viewers have been looking for, and what was searched just now."}),e.jsx(_,{message:a}),e.jsx(ie,{tiles:[{label:"searches",value:w((d==null?void 0:d.searches)??0),icon:"search",tone:"info"},{label:"distinct queries",value:w((d==null?void 0:d.queries)??0),icon:"list",tone:"data"},{label:"viewers searching",value:w((d==null?void 0:d.viewers)??0),icon:"people",tone:"note"},{label:"history kept",value:`${(t==null?void 0:t.retentionDays)??30} days`,small:!0,icon:"clock"}]}),i?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"What the house looks for",intro:"Queries the search tab ran, grouped without regard to case and labelled with the most recent spelling. Instant search asks from the second character, so a title typed slowly leaves its prefixes here too.",icon:"search",tone:"info",actions:e.jsx(q,{label:"Window",children:e.jsxs("select",{value:s,onChange:c=>n(Number(c.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"})]})}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Query"}),e.jsx("th",{className:"num",children:"Searches"}),e.jsx("th",{className:"num",children:"Viewers"}),e.jsx("th",{children:"Last searched"})]})}),e.jsx("tbody",{children:l.length===0?e.jsx(Y,{columns:4,children:"Nothing searched in this window."}):l.map(c=>e.jsxs("tr",{children:[e.jsx("td",{children:c.query}),e.jsx("td",{className:"num",children:w(c.searches)}),e.jsx("td",{className:"num",children:w(c.viewers)}),e.jsx("td",{className:"muted nowrap",children:F(c.lastAt)})]},c.query))})]})})}),e.jsx(M,{title:"As it happened",intro:"The log, newest first — the query exactly as it was typed, and who typed it. This is the one to read when somebody says search is not finding something.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{children:"Viewer"}),e.jsx("th",{children:"Query"})]})}),e.jsx("tbody",{children:r.length===0?e.jsx(Y,{columns:3,children:"No searches in this window."}):r.map((c,o)=>e.jsxs("tr",{children:[e.jsx("td",{className:"muted nowrap",children:F(c.occurredAt)}),e.jsx("td",{children:c.username||e.jsx(N,{tone:"warn",children:c.userId||"unknown"})}),e.jsx("td",{children:c.query})]},`${c.occurredAt}:${o}`))})]})})})]})]})}function hn(s,n){if(n===0)return s>0?"new this week":"no change";const t=Math.round((s-n)/n*100);return`${t>0?"+":""}${t}% vs last week`}function Ma(){const{data:s,error:n,loading:t}=J("/admin/api/views",{pollMs:6e4}),a=(s==null?void 0:s.daily)??[],i=(s==null?void 0:s.hourly)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Views",intro:"How often people reach Memby’s home screen. This measures app use, not playback streams."}),e.jsx(_,{message:n}),t?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ie,{tiles:[{label:hn((s==null?void 0:s.today.visits)??0,(s==null?void 0:s.lastWeek.visits)??0),value:w(s==null?void 0:s.today.visits),icon:"overview",tone:"data"},{label:hn((s==null?void 0:s.today.viewers)??0,(s==null?void 0:s.lastWeek.viewers)??0),value:w(s==null?void 0:s.today.viewers),icon:"people",tone:"note"},{label:"busiest time today",value:(s==null?void 0:s.busiestHour)||"—",small:!0,icon:"clock",tone:"info"}]}),e.jsx(M,{title:"Visits by day",intro:"One visit is a signed-in home-screen opening. Viewers are distinct signed-in profiles.",icon:"chart",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Day"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(Y,{columns:3,children:"No home-screen visits yet."}):a.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:w(l.visits)}),e.jsx("td",{className:"num",children:w(l.viewers)})]},l.label))})]})})}),e.jsx(M,{title:"Today by hour",intro:"Local New Zealand time. Use this to see when viewers are opening Memby.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Hour"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(Y,{columns:3,children:"No home-screen visits yet today."}):i.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:w(l.visits)}),e.jsx("td",{className:"num",children:w(l.viewers)})]},l.label))})]})})})]})]})}const $a=s=>s.mediaType==="episode"?`${s.seriesTitle} S${String(s.seasonNumber).padStart(2,"0")}E${String(s.episodeNumber).padStart(2,"0")}`:s.title;function Ra(){var i;const s=J("/admin/api/media-reports",{pollMs:15e3}),{busy:n,run:t}=ne(),a=(l,r)=>t(`${l.id}-${r}`,async()=>{await D.post(`/admin/api/media-reports/${l.id}/status`,{status:r}),await s.reload()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Media reports",intro:"Viewer-reported problems and the individual replacement searches they asked Memby to start."}),s.loading?e.jsx(V,{rows:4}):e.jsx(M,{title:"Open and recent reports",intro:"A replacement always targets one film or one episode. Existing files stay in place while Radarr or Sonarr applies its normal import policy.",icon:"inbox",children:(i=s.data)!=null&&i.reports.length?e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Media"}),e.jsx("th",{children:"Report"}),e.jsx("th",{children:"Viewer"}),e.jsx("th",{children:"Replacement"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Reported"}),e.jsx("th",{children:"Actions"})]})}),e.jsx("tbody",{children:s.data.reports.map(l=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:$a(l)}),e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.title})]}),e.jsxs("td",{children:[l.reason.replaceAll("_"," "),l.comment?e.jsxs(e.Fragment,{children:[e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.comment})]}):null]}),e.jsxs("td",{children:[l.reportedByUsername,e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.reportedByDevice||"Unknown device"})]}),e.jsx("td",{children:e.jsx(N,{tone:l.replacementRequested?"note":void 0,children:l.replacementRequested?l.replacementStatus||"Requested":"Not requested"})}),e.jsx("td",{children:e.jsx(N,{tone:l.status==="resolved"?"ok":l.status==="dismissed"?void 0:"warn",children:l.status})}),e.jsx("td",{className:"nowrap muted",children:F(l.createdAt)}),e.jsxs("td",{children:[e.jsx($,{size:"sm",variant:"quiet",busy:n===`${l.id}-acknowledged`,onClick:()=>void a(l,"acknowledged"),children:"Acknowledge"})," ",e.jsx($,{size:"sm",variant:"quiet",busy:n===`${l.id}-resolved`,onClick:()=>void a(l,"resolved"),children:"Resolve"})," ",e.jsx($,{size:"sm",variant:"quiet",busy:n===`${l.id}-dismissed`,onClick:()=>void a(l,"dismissed"),children:"Dismiss"})]})]},l.id))})]})}):e.jsx(ee,{children:"No media problems have been reported."})})]})}const un={user:"",kind:"",channel:"",status:"",source:"",q:"",from:"",to:""},Ta=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:90,label:"90 days"}],Ke=100,Dn={sent:"ok",delivered:"ok",failed:"bad",pending:"warn",skipped:"idle"},Aa={"in-app":"note",broadcast:"info",webhook:"data"},os={"in-app":"In-app",broadcast:"Broadcast",webhook:"Webhook"};function Se(s){if(!s)return"—";const n=s.replace(/[-_.:]+/g," ").trim();return n.charAt(0).toUpperCase()+n.slice(1)}function is(s,n){return(s??[]).map(t=>e.jsxs("option",{value:t.value,children:[n(t.value)," (",t.count,")"]},t.value))}function Ea(){var y,v,b,T;const[s,n]=g.useState(7),[t,a]=g.useState(un),[i,l]=g.useState(0),[r,d]=g.useState(null),c=g.useMemo(()=>Ve({...t,days:t.from?void 0:s,limit:Ke,offset:i*Ke}),[t,s,i]),o=J(`/admin/api/notification-log${c}`),u=o.data,p=k=>{a(I=>({...I,...k})),l(0),d(null)},m=Object.values(t).some(k=>k!==""),x=u==null?void 0:u.totals,f=(u==null?void 0:u.retentionDays)??90,h=(u==null?void 0:u.entries.length)??0,j=!u||u.total===0?0:i*Ke+1;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Notifications",intro:"Everything Memby sent — a viewer's own news, the bar every television draws, and each outbound webhook — with what became of it. Every feature reports through one notification service, so this is the whole trail rather than whichever half a feature remembered to log."}),e.jsx(_,{message:o.error}),x?e.jsx(ie,{tiles:[{label:"Sent",value:w(x.sent),icon:"send",tone:"ok"},{label:"Confirmed",value:w(x.delivered),icon:"check",tone:"ok"},{label:"Failed",value:w(x.failed),icon:"alert",tone:x.failed>0?"bad":void 0},{label:"Skipped",value:w(x.skipped),icon:"filter",tone:"idle"},{label:"People reached",value:w(x.users),icon:"people",tone:"note"},{label:"History kept",value:`${f} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(q,{label:"Window",children:e.jsx(Ue,{value:t.from?-1:s,options:Ta.map(k=>({value:k.value,label:k.label})),onChange:k=>{n(k),p({from:"",to:""})}})}),e.jsx(q,{label:"Person",children:e.jsxs("select",{value:t.user,onChange:k=>p({user:k.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),((u==null?void 0:u.users)??[]).map(k=>e.jsx("option",{value:k.id,children:k.username||k.id},k.id))]})}),e.jsx(q,{label:"Type",children:e.jsxs("select",{value:t.kind,onChange:k=>p({kind:k.target.value}),children:[e.jsx("option",{value:"",children:"Any type"}),is((y=u==null?void 0:u.facets)==null?void 0:y.kinds,Se)]})}),e.jsx(q,{label:"Channel",children:e.jsxs("select",{value:t.channel,onChange:k=>p({channel:k.target.value}),children:[e.jsx("option",{value:"",children:"Any channel"}),is((v=u==null?void 0:u.facets)==null?void 0:v.channels,k=>os[k]??Se(k))]})}),e.jsx(q,{label:"Status",children:e.jsxs("select",{value:t.status,onChange:k=>p({status:k.target.value}),children:[e.jsx("option",{value:"",children:"Any status"}),is((b=u==null?void 0:u.facets)==null?void 0:b.statuses,Se)]})}),e.jsx(q,{label:"Source",children:e.jsxs("select",{value:t.source,onChange:k=>p({source:k.target.value}),children:[e.jsx("option",{value:"",children:"Any service"}),is((T=u==null?void 0:u.facets)==null?void 0:T.sources,Se)]})}),e.jsx(q,{label:"From",children:e.jsx("input",{type:"date",value:t.from,onChange:k=>p({from:k.target.value})})}),e.jsx(q,{label:"To",children:e.jsx("input",{type:"date",value:t.to,onChange:k=>p({to:k.target.value})})}),e.jsx(q,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:t.q,placeholder:"Title, message, error or person",onChange:k=>p({q:k.target.value})})}),e.jsx("div",{className:"filter-actions",children:m?e.jsx($,{variant:"quiet",size:"sm",onClick:()=>{a(un),l(0),d(null)},children:"Clear"}):null})]}),o.loading&&!u?e.jsx(V,{}):null,u&&u.days.length>1?e.jsx(M,{title:"Notifications per day",intro:"Failures and deliberate skips are counted beside the deliveries, because a quiet week and a week nothing was allowed to send look identical otherwise.",icon:"chart",tone:"info",children:e.jsx(hs,{data:u.days,labelOf:k=>k.day,valueOf:k=>k.sent+k.delivered+k.failed+k.skipped,toneOf:k=>k.failed>0?"bad":void 0,title:k=>`${k.day}: ${k.sent+k.delivered} sent, ${k.failed} failed, ${k.skipped} skipped`})}):null,u?e.jsx(M,{title:"History",intro:"Newest first. A row says who, what and whether it worked on its own; open one for the whole message and the delivery response.",icon:"send",tone:"ok",actions:e.jsx("span",{className:"filter-summary",children:u.total===0?"nothing matches":`${w(j)}–${w(j+h-1)} of ${w(u.total)}`}),footer:u.total>Ke?e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",disabled:i===0,onClick:()=>l(i-1),children:"Newer"}),e.jsx($,{size:"sm",disabled:(i+1)*Ke>=u.total,onClick:()=>l(i+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Recipient"}),e.jsx("th",{children:"Type"}),e.jsx("th",{children:"Title"}),e.jsx("th",{children:"Channel"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"Status"}),e.jsx("th",{"aria-label":"Details"})]})}),e.jsx("tbody",{children:u.entries.length===0?e.jsx(Y,{columns:8,children:"No notifications match these filters."}):u.entries.map(k=>e.jsxs(g.Fragment,{children:[e.jsx(Ia,{entry:k,open:r===k.id,onToggle:()=>d(r===k.id?null:k.id)}),r===k.id?e.jsx(La,{entry:k}):null]},k.id))})]})})}):null]})}function Ia({entry:s,open:n,onToggle:t}){return e.jsxs("tr",{"data-selected":n||void 0,children:[e.jsx("td",{className:"nowrap muted",children:F(s.occurredAt)}),e.jsx("td",{children:s.userId?e.jsx(Z,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(s.userId)}`,children:s.username||s.userId}):s.target?e.jsx("span",{className:"muted",children:s.target}):e.jsx("span",{className:"quiet",children:"Everyone"})}),e.jsx("td",{className:"mono",children:s.kind||"—"}),e.jsx("td",{children:s.title||e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"nowrap",children:e.jsx(N,{tone:Aa[s.channel],children:os[s.channel]??s.channel})}),e.jsx("td",{className:"muted",children:Se(s.source)}),e.jsx("td",{className:"nowrap",children:e.jsx(N,{tone:Dn[s.status]??"idle",children:s.status})}),e.jsx("td",{className:"nowrap",children:e.jsx($,{size:"sm",variant:"quiet",onClick:t,icon:n?"close":"list",children:n?"Close":"Details"})})]})}function La({entry:s}){const n=Object.entries(s.metadata??{}).filter(([,t])=>t!=null&&String(t)!=="");return e.jsx("tr",{className:"detail-row",children:e.jsx("td",{colSpan:8,children:e.jsxs("section",{className:"logdrawer","aria-label":`Notification ${s.id}`,children:[e.jsxs("header",{className:"logdrawer-head",children:[e.jsxs("div",{children:[e.jsxs("p",{className:"logdrawer-place",children:[e.jsx("span",{children:os[s.channel]??s.channel}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),Se(s.source)]}),e.jsx("b",{children:s.title||Se(s.kind)}),s.detail?e.jsx("p",{className:"logdrawer-error",children:s.detail}):null]}),e.jsx("div",{className:"logdrawer-actions",children:e.jsx(N,{tone:Dn[s.status]??"idle",children:s.status})})]}),e.jsxs("div",{className:"logdrawer-grid",children:[e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Delivery"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Sent"}),e.jsx("dd",{children:F(s.occurredAt)}),s.eventAt?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Event"}),e.jsx("dd",{children:F(s.eventAt)})]}):null,e.jsx("dt",{children:"Channel"}),e.jsx("dd",{children:os[s.channel]??s.channel}),e.jsx("dt",{children:"Type"}),e.jsx("dd",{children:s.kind||"—"}),e.jsx("dt",{children:"Service"}),e.jsx("dd",{children:Se(s.source)}),e.jsx("dt",{children:"Took"}),e.jsxs("dd",{children:[s.durationMs,"ms"]})]})]}),e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Recipient"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Person"}),e.jsx("dd",{children:s.userId?s.username||s.userId:"the whole household"}),s.target?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Destination"}),e.jsx("dd",{children:s.target})]}):null,s.itemId?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Title id"}),e.jsx("dd",{children:s.itemId})]}):null,s.sourceKey?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Source key"}),e.jsx("dd",{children:s.sourceKey})]}):null]})]}),n.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Context"}),e.jsx("dl",{children:n.map(([t,a])=>e.jsxs(g.Fragment,{children:[e.jsx("dt",{children:Se(t)}),e.jsx("dd",{children:String(a)})]},t))})]}):null]}),s.body?e.jsxs("div",{className:"logdrawer-raw",children:[e.jsx("h4",{children:"Message"}),e.jsx("p",{className:"notification-body",children:s.body})]}):null]})})})}const qa=s=>s==="detected"?"ok":s==="failed"?"bad":s==="no_match"?"warn":"info",Da=s=>s==="no_match"?"no match":s,mn=s=>({"live-playback":"Live playback","tracearr-next":"Next episode","tracearr-binge-prefetch":"Binge look-ahead","multi-user-demand":"Multiple viewers"}[s]??s)||"Unknown",pn=(s,n)=>s>0&&n>0?`S${String(s).padStart(2,"0")}E${String(n).padStart(2,"0")}`:"Episode";function Fa(){var h,j,y,v;const s=J("/admin/api/credits?limit=150",{pollMs:15e3}),{wrap:n}=ae(),{busy:t,run:a}=ne(),[i,l]=g.useState(),[r,d]=g.useState(!1);g.useEffect(()=>{!r&&s.data&&l(s.data.settings)},[s.data,r]);const c=b=>{l(T=>T&&{...T,...b}),d(!0)},o=()=>{i&&a("save",async()=>{const b=await n(()=>D.put("/admin/api/credits",i),"Credits scanning settings saved.");b&&(s.set(b),l(b.settings),d(!1))})},u=((h=s.data)==null?void 0:h.history)??[],p=((j=s.data)==null?void 0:j.pending)??[],m=u.filter(b=>b.outcome==="detected").length,x=u.filter(b=>b.outcome==="no_match").length,f=u.filter(b=>b.outcome==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Credits detection",intro:"Control how far ahead Memby scans and see why each episode was selected, what the detector found, and when it may be tried again."}),e.jsx(_,{message:s.error}),s.loading||!i?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[(y=s.data)!=null&&y.enabled?null:e.jsx(pe,{tone:"warn",children:"Credits detection is disabled in the gateway environment. These settings will be retained for the next time it is enabled."}),e.jsx(ie,{tiles:[{label:"Waiting candidates",value:w((v=s.data)==null?void 0:v.queueDepth),icon:"clock",tone:p.length?"info":void 0},{label:"Detected in this history",value:w(m),icon:"check",tone:"ok"},{label:"No match",value:w(x),icon:"search",tone:x?"warn":void 0},{label:"Failed",value:w(f),icon:"alert",tone:f?"bad":void 0}]}),e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Candidate controls",intro:"The worker remains single-file and scans one episode at a time. These values control what is allowed to wait and how far prediction looks ahead.",icon:"sliders",tone:"note",footer:e.jsx($,{variant:"primary",icon:"check",busy:t==="save",disabled:!r,onClick:o,children:"Save settings"}),children:e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Candidate limit",hint:"Maximum episodes waiting in the priority queue. Stronger candidates displace weaker ones when it is full.",children:e.jsx("input",{type:"number",min:1,max:100,value:i.candidateLimit,onChange:b=>c({candidateLimit:Number(b.target.value)})})}),e.jsx(q,{label:"Ordinary look-ahead",hint:"Episodes prepared ahead of a normally paced viewer.",children:e.jsx("input",{type:"number",min:1,max:10,value:i.prefetchEpisodes,onChange:b=>c({prefetchEpisodes:Number(b.target.value)})})}),e.jsx(q,{label:"Maximum look-ahead",hint:"Upper bound for fast binge viewing; must not be below the ordinary look-ahead.",children:e.jsx("input",{type:"number",min:i.prefetchEpisodes,max:20,value:i.maxPrefetch,onChange:b=>c({maxPrefetch:Number(b.target.value)})})}),e.jsx(q,{label:"Retry delay (hours)",hint:"After any speculative attempt, keep that episode out of refreshes for this long. Set 0 to allow every refresh.",children:e.jsx("input",{type:"number",min:0,max:720,value:i.retryHours,onChange:b=>c({retryHours:Number(b.target.value)})})})]})}),e.jsxs(M,{title:"How selection works",icon:"sparkle",tone:"data",children:[e.jsx("p",{className:"muted",children:"Recent viewing predicts the next few episodes. Priority favours a programme playing now, then the next episode, fast viewing, and episodes several people are approaching."}),e.jsx("p",{className:"muted",children:"A completed speculative attempt enters the retry delay even when no marker was found. Live playback can still raise an immediate candidate because somebody is waiting for it."})]})]}),e.jsx(M,{title:"Waiting candidates",intro:"The exact worker order after marker checks and retry cooldowns. A refresh may replace this list as viewing changes.",icon:"list",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Episode"}),e.jsx("th",{children:"Reason"}),e.jsx("th",{className:"num",children:"Priority"}),e.jsx("th",{className:"num",children:"Viewers"}),e.jsx("th",{children:"Demand seen"}),e.jsx("th",{children:"Item ID"})]})}),e.jsx("tbody",{children:p.length===0?e.jsx(Y,{columns:6,children:"No episodes are waiting to be scanned."}):p.map(b=>e.jsxs("tr",{children:[e.jsx("td",{children:pn(b.season,b.episode)}),e.jsx("td",{children:e.jsx(N,{tone:"info",children:mn(b.reason)})}),e.jsx("td",{className:"num",children:w(b.priority)}),e.jsx("td",{className:"num",children:w(b.userCount)}),e.jsx("td",{className:"nowrap muted",title:F(b.lastViewed),children:be(b.lastViewed)}),e.jsx("td",{className:"mono muted",children:b.itemId})]},b.itemId))})]})})}),e.jsx(M,{title:"Scan history",intro:"Completed worker attempts, newest first. Repeated item IDs make an ineffective retry delay visible immediately.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Finished"}),e.jsx("th",{children:"Programme"}),e.jsx("th",{children:"Selected because"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Marker"}),e.jsx("th",{children:"Evidence"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:u.length===0?e.jsx(Y,{columns:7,children:"No credits scans have completed yet."}):u.map(b=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(b.finishedAt),children:be(b.finishedAt)}),e.jsxs("td",{children:[e.jsx("b",{children:b.seriesName||b.itemName||b.itemId}),e.jsxs("span",{className:"table-sub",children:[pn(b.season,b.episode),b.itemName&&b.seriesName?` · ${b.itemName}`:""]})]}),e.jsxs("td",{children:[e.jsx(N,{tone:"info",children:mn(b.reason)}),e.jsxs("span",{className:"table-sub",children:["priority ",b.priority]})]}),e.jsxs("td",{children:[e.jsx(N,{tone:qa(b.outcome),children:Da(b.outcome)}),b.error?e.jsx("span",{className:"table-sub",children:b.error}):null]}),e.jsx("td",{className:"nowrap",children:b.markerMs>0?ye(b.markerMs):"—"}),e.jsxs("td",{className:"muted",children:[b.method||"visual",b.confidence>0?` · ${as(b.confidence)}`:"",b.frames>0?` · ${b.frames} frames`:""]}),e.jsx("td",{className:"num muted",children:ye(b.durationMs)})]},b.id))})]})})})]})]})}function Oa(){return e.jsx(U,{title:"No such page",intro:"That address is not part of the console. Use the search in the bar above, or the sections on the left."})}function Pa(){return e.jsx(_n,{children:e.jsx(et,{children:e.jsx(at,{children:e.jsx(ht,{children:e.jsx(Un,{children:e.jsxs(W,{path:"/admin",element:e.jsx(ct,{}),children:[e.jsx(W,{index:!0,element:e.jsx(mt,{})}),e.jsx(W,{path:"activity",element:e.jsx(xt,{})}),e.jsx(W,{path:"accounts",element:e.jsx(jt,{})}),e.jsx(W,{path:"accounts/:userId",element:e.jsx(gt,{})}),e.jsx(W,{path:"accounts/:userId/settings",element:e.jsx(Nt,{})}),e.jsx(W,{path:"clients",element:e.jsx(Ct,{})}),e.jsx(W,{path:"logins",element:e.jsx($t,{})}),e.jsx(W,{path:"devices/:deviceId",element:e.jsx(Et,{})}),e.jsx(W,{path:"library",element:e.jsx(It,{})}),e.jsx(W,{path:"requests",element:e.jsx(Lt,{})}),e.jsx(W,{path:"recommendations",element:e.jsx(qt,{})}),e.jsx(W,{path:"inspector",element:e.jsx(Ot,{})}),e.jsx(W,{path:"hero",element:e.jsx(Wt,{})}),e.jsx(W,{path:"metadata-hero",element:e.jsx(Vt,{})}),e.jsx(W,{path:"features",element:e.jsx(Ht,{})}),e.jsx(W,{path:"playback",element:e.jsx(zt,{})}),e.jsx(W,{path:"subtitles",element:e.jsx(Kt,{})}),e.jsx(W,{path:"credits",element:e.jsx(Fa,{})}),e.jsx(W,{path:"updates",element:e.jsx(Gt,{})}),e.jsx(W,{path:"tasks",element:e.jsx(si,{})}),e.jsx(W,{path:"tasks/:taskId",element:e.jsx(ri,{})}),e.jsx(W,{path:"integrations",element:e.jsx(ui,{})}),e.jsx(W,{path:"integrations/webhooks",element:e.jsx(gi,{})}),e.jsx(W,{path:"integrations/:integrationId",element:e.jsx(Ni,{})}),e.jsx(W,{path:"maintenance",element:e.jsx(Mi,{})}),e.jsx(W,{path:"runtime",element:e.jsx($i,{})}),e.jsx(W,{path:"settings",element:e.jsx(Ei,{})}),e.jsx(W,{path:"imports",element:e.jsx(Di,{})}),e.jsx(W,{path:"logs",element:e.jsx(va,{})}),e.jsx(W,{path:"journeys",element:e.jsx(ba,{})}),e.jsx(W,{path:"journeys/:userId",element:e.jsx(Na,{})}),e.jsx(W,{path:"views",element:e.jsx(Ma,{})}),e.jsx(W,{path:"engagement",element:e.jsx(Sa,{})}),e.jsx(W,{path:"searches",element:e.jsx(Ca,{})}),e.jsx(W,{path:"media-reports",element:e.jsx(Ra,{})}),e.jsx(W,{path:"notifications",element:e.jsx(Ea,{})}),e.jsx(W,{path:"overview",element:e.jsx(As,{to:"/admin",replace:!0})}),e.jsx(W,{path:"ratings",element:e.jsx(As,{to:"/admin/integrations/mdblist",replace:!0})}),e.jsx(W,{path:"*",element:e.jsx(Oa,{})})]})})})})})})}const Fn=document.getElementById("root");if(!Fn)throw new Error("the console has no root element to render into");vn(Fn).render(e.jsx(g.StrictMode,{children:e.jsx(Pa,{})})); diff --git a/admin-ui/dist/assets/index-C6xrkGsh.js b/admin-ui/dist/assets/index-C6xrkGsh.js new file mode 100644 index 0000000..bbbfebe --- /dev/null +++ b/admin-ui/dist/assets/index-C6xrkGsh.js @@ -0,0 +1,11 @@ +import{r as v,a as Pn,u as ws,L as Z,b as ks,m as vs,N as jn,O as _n,c as Be,B as Un,R as Wn,d as W,e as Es}from"./router-D9WH5XEU.js";(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))i(a);new MutationObserver(a=>{for(const l of a)if(l.type==="childList")for(const r of l.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&i(r)}).observe(document,{childList:!0,subtree:!0});function t(a){const l={};return a.integrity&&(l.integrity=a.integrity),a.referrerPolicy&&(l.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?l.credentials="include":a.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function i(a){if(a.ep)return;a.ep=!0;const l=t(a);fetch(a.href,l)}})();var gn={exports:{}},cs={};/** + * @license React + * react-jsx-runtime.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var Bn=v,Vn=Symbol.for("react.element"),Hn=Symbol.for("react.fragment"),zn=Object.prototype.hasOwnProperty,Kn=Bn.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Gn={key:!0,ref:!0,__self:!0,__source:!0};function vn(s,n,t){var i,a={},l=null,r=null;t!==void 0&&(l=""+t),n.key!==void 0&&(l=""+n.key),n.ref!==void 0&&(r=n.ref);for(i in n)zn.call(n,i)&&!Gn.hasOwnProperty(i)&&(a[i]=n[i]);if(s&&s.defaultProps)for(i in n=s.defaultProps,n)a[i]===void 0&&(a[i]=n[i]);return{$$typeof:Vn,type:s,key:l,ref:r,props:a,_owner:Kn.current}}cs.Fragment=Hn;cs.jsx=vn;cs.jsxs=vn;gn.exports=cs;var e=gn.exports,fn,As=Pn;fn=As.createRoot,As.hydrateRoot;const bn={overview:"M4 13h6V4H4zm0 7h6v-4H4zm10 0h6v-9h-6zm0-16v4h6V4z",library:"M3 5h18v14H3zM7 5v14M17 5v14M3 9.5h4M3 14.5h4M17 9.5h4M17 14.5h4",people:"M15 19v-1.2a3.3 3.3 0 0 0-3.3-3.3H6.8A3.3 3.3 0 0 0 3.5 17.8V19M9.2 11a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4ZM17 10.6a3 3 0 0 0-1.4-5.7M20.5 19v-1.2a3.3 3.3 0 0 0-2.4-3.2",person:"M18 20v-1.5a4 4 0 0 0-4-4h-4a4 4 0 0 0-4 4V20M12 10.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z",tv:"M4 5h16v10H4zM9 19h6M12 15v4M8 2.5 12 5l4-2.5",sliders:"M4 7h10M18 7h2M4 17h2m4 0h10M14 4v6M6 14v6",clock:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 7.2V12l3 1.8",pulse:"M3 12h3.5L9 19l5-14 2.5 7H21",chart:"M4 19V9m5 10V5m5 14v-7m5 7V3",chip:"M8 8h8v8H8zM4.5 4.5h15v15h-15zM9 2v2.5M15 2v2.5M9 19.5V22M15 19.5V22M2 9h2.5M2 15h2.5M19.5 9H22M19.5 15H22",database:"M12 8.2c4.4 0 8-1.2 8-2.6S16.4 3 12 3 4 4.2 4 5.6s3.6 2.6 8 2.6ZM4 5.6v12.8C4 19.8 7.6 21 12 21s8-1.2 8-2.6V5.6M4 12c0 1.4 3.6 2.6 8 2.6s8-1.2 8-2.6",download:"M12 3.5v10m0 0 4-4m-4 4-4-4M4.5 18h15",upload:"M12 16V4m0 0L8 8m4-4 4 4M5 13v6h14v-6",sync:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",search:"M10.5 17a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Zm4.6-1.9L20 20",star:"m12 3.2 2.6 5.4 5.9.8-4.3 4.1 1 5.9-5.2-2.8-5.2 2.8 1-5.9L3.5 9.4l5.9-.8L12 3.2Z",sparkle:"m10 3 1.5 4.3L16 8.8l-4.5 1.5L10 14.6 8.5 10.3 4 8.8l4.5-1.5L10 3ZM17.5 14l.9 2.4 2.6.9-2.6.9-.9 2.4-.9-2.4-2.6-.9 2.6-.9.9-2.4Z",bell:"M6.2 9.5a5.8 5.8 0 1 1 11.6 0c0 4.6 2.2 5.9 2.2 5.9H4s2.2-1.3 2.2-5.9M10 19.5a2 2 0 0 0 4 0",shield:"m12 3 7.5 3v5.4c0 5-3.2 8.2-7.5 9.6-4.3-1.4-7.5-4.6-7.5-9.6V6L12 3Zm-2.6 8.7 1.9 1.9 3.6-3.6",wrench:"m14.5 6.5 3-3 3 3-3 3M9 15l-5.5 5.5M13 4a5 5 0 0 0 6.5 6.5L10 20l-6-6 9.5-9.5Z",play:"M8 5.2v13.6L19 12 8 5.2ZM4 5v14",list:"M4 7h16M4 12h16M4 17h10",inbox:"M4 7h16v13H4zM8 4h8v3M8 12h8M8 16h5",send:"M21 3 10.5 13.5M21 3l-6.8 18-3.7-7.5L3 10z",history:"M3.5 12a8.5 8.5 0 1 0 2.8-6.3M3.5 4v4h4M12 7.5V12l3 1.8",check:"m5 12.5 4.5 4.5L19 7.5",alert:"M12 8.5v5m0 3.2h.01M10.3 4.4 2.7 17.5a2 2 0 0 0 1.7 3h15.2a2 2 0 0 0 1.7-3L13.7 4.4a2 2 0 0 0-3.4 0Z",power:"M12 3v9M7.5 6.2a7.5 7.5 0 1 0 9 0",key:"M14.5 3a6.5 6.5 0 1 0 3.4 12L19 14h2v-2h2V9.5l-2.5-2.5A6.5 6.5 0 0 0 14.5 3Zm-2.6 4.6a1.6 1.6 0 1 1-2.3 2.3 1.6 1.6 0 0 1 2.3-2.3Z",captions:"M4 5.5h16v13H4zM7 15h5m3 0h2M7 11h3m2 0h5",journey:"M4 6h5v5h6v7h5M7 3 4 6l3 3m10 6 3 3-3 3",plug:"M9 3v6M15 3v6M6.5 9h11v3.5a5.5 5.5 0 0 1-11 0zM12 18v3",calendar:"M4 6h16v15H4zM8 3v5M16 3v5M4 11h16",trash:"M4 7h16M9 7V4.5h6V7M6.5 7l1 13h9l1-13M10 11v5M14 11v5",plus:"M12 5v14M5 12h14",close:"M6 6l12 12M18 6 6 18",caret:"m6 9 6 6 6-6",external:"M14 4h6v6M20 4l-9 9M18 14v5.5H4.5V6H10",refresh:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",filter:"M3.5 5.5h17l-6.5 7.5V20l-4-2v-5L3.5 5.5Z",menu:"M4 7h16M4 12h16M4 17h16",globe:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3.5 9h17M3.5 15h17M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18Z",logout:"M15 17l5-5-5-5M20 12H9M12 4H5v16h7"};function se({name:s,className:n}){const t=bn[s];return t?e.jsx("svg",{className:n??"ico",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:e.jsx("path",{d:t})}):null}function Qe({name:s,tone:n}){return bn[s]?e.jsx("span",{className:"glyph","data-tone":n,children:e.jsx(se,{name:s})}):null}const Oe=[{id:"everyday",label:"Everyday",defaultCollapsed:!1,items:[{id:"overview",path:"/admin",label:"Overview",title:"Overview",intro:"What the gateway is doing right now.",icon:"overview"},{id:"activity",path:"/admin/activity",label:"Activity",title:"Activity",intro:"Every administrative event, newest first.",icon:"bell",badge:"notifications"},{id:"accounts",path:"/admin/accounts",label:"Users",title:"Users",intro:"Who uses Memby, and the devices they are signed in on.",icon:"people"},{id:"requests",path:"/admin/requests",label:"Media requests",title:"Media requests",intro:"Who can ask for something the library does not have.",icon:"inbox"},{id:"notifications",path:"/admin/notifications",label:"Notifications",title:"Notifications",intro:"Everything Memby sent: who it went to, over which channel, and whether it worked.",icon:"send"},{id:"media-reports",path:"/admin/media-reports",label:"Media reports",title:"Media reports",intro:"Problems viewers reported with a film or episode.",icon:"alert"},{id:"updates",path:"/admin/updates",label:"App updates",title:"App updates",intro:"Publish an optional or a required client update.",icon:"upload"},{id:"logs",path:"/admin/logs",label:"Logs",title:"Logs",intro:"Structured gateway events as they happen.",icon:"list"}]},{id:"people",label:"Devices & access",defaultCollapsed:!0,items:[{id:"account",path:"/admin/accounts/:userId",label:"User",title:"User",intro:"Devices, recommendation setup and synced settings for one person.",icon:"person",hidden:!0},{id:"settings-history",path:"/admin/accounts/:userId/settings",label:"Settings history",title:"Settings history",intro:"Every change to one person's synced settings, and which devices took it.",icon:"sliders",hidden:!0},{id:"clients",path:"/admin/clients",label:"Devices",title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands.",icon:"tv"},{id:"logins",path:"/admin/logins",label:"Sign-ins",title:"Sign-in history",intro:"Every connection attempt: who, which television, from where, and whether it got in.",icon:"key"},{id:"device",path:"/admin/devices/:deviceId",label:"Device",title:"Device",intro:"One television: how often it connects, at what times, and from which addresses.",icon:"tv",hidden:!0}]},{id:"content",label:"Content & discovery",defaultCollapsed:!0,items:[{id:"library",path:"/admin/library",label:"Library",title:"Library",intro:"Import and inspect the catalogue Memby ranks.",icon:"library"},{id:"hero",path:"/admin/hero",label:"Home hero",title:"Home hero",intro:"Choose films or television shows for the launcher spotlight while recent releases fill the remaining places.",icon:"star"},{id:"metadata-hero",path:"/admin/metadata-hero",label:"Metadata hero",title:"Metadata hero",intro:"Order the focused-title information shown above browse rows on every television.",icon:"tv"},{id:"recommendations",path:"/admin/recommendations",label:"For You",title:"For You",intro:"The prepared pools personalised rows are drawn from.",icon:"sparkle"},{id:"inspector",path:"/admin/inspector",label:"Score inspector",title:"Score inspector",intro:"Re-run the ranker for one person and read every component.",icon:"search"}]},{id:"experience",label:"Viewing experience",defaultCollapsed:!0,items:[{id:"features",path:"/admin/features",label:"Client configuration",title:"Client configuration",intro:"Control everything the thin TV client renders, without releasing an APK.",icon:"sliders"},{id:"playback",path:"/admin/playback",label:"Playback",title:"Playback",intro:"Presentation policy sent with every playback launch.",icon:"play"},{id:"subtitles",path:"/admin/subtitles",label:"Subtitles",title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from.",icon:"captions"},{id:"credits",path:"/admin/credits",label:"Credits detection",title:"Credits detection",intro:"Control predictive scanning and review every completed credits scan.",icon:"clock"}]},{id:"operations",label:"Operations",defaultCollapsed:!0,items:[{id:"tasks",path:"/admin/tasks",label:"Scheduled tasks",title:"Scheduled tasks",intro:"What the gateway does in the background, when it last ran and whether it worked.",icon:"clock"},{id:"task",path:"/admin/tasks/:taskId",label:"Task",title:"Task",intro:"One scheduled task: its cadence, its switch and its own run history.",icon:"clock",hidden:!0},{id:"imports",path:"/admin/imports",label:"Imports",title:"Imports",intro:"Catalogue synchronisation history.",icon:"database"},{id:"runtime",path:"/admin/runtime",label:"Runtime",title:"Runtime",intro:"Goroutines, memory and the background workers inside the gateway process.",icon:"chip"},{id:"maintenance",path:"/admin/maintenance",label:"Maintenance",title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time.",icon:"wrench"},{id:"gateway-settings",path:"/admin/settings",label:"Gateway settings",title:"Gateway settings",intro:"Timezone, logging and the other server-level settings for this gateway.",icon:"sliders",hidden:!0}]},{id:"integrations",label:"Integrations",defaultCollapsed:!0,items:[{id:"integrations",path:"/admin/integrations",label:"Overview",title:"Integrations",intro:"Every external service Memby depends on: configured, working, and what it last did.",icon:"plug"},{id:"integration",path:"/admin/integrations/:integrationId",label:"Integration",title:"Integration",intro:"One external service: its switch, its settings, its jobs and its run history.",icon:"plug",hidden:!0},{id:"webhooks",path:"/admin/integrations/webhooks",label:"Event webhooks",title:"Event webhooks",intro:"Send administrative events to Discord and, in time, elsewhere.",icon:"send"}]},{id:"insights",label:"Insights",defaultCollapsed:!0,items:[{id:"views",path:"/admin/views",label:"Views",title:"App views",intro:"Home-screen visits, viewers and the times Memby is used.",icon:"overview"},{id:"searches",path:"/admin/searches",label:"Searches",title:"Searches",intro:"What viewers have been looking for, and what was searched just now.",icon:"search"},{id:"journeys",path:"/admin/journeys",label:"Journeys",title:"User journeys",intro:"How viewers move through Memby, use features and complete flows.",icon:"journey"},{id:"engagement",path:"/admin/engagement",label:"Row engagement",title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row.",icon:"chart"}]}],Zn=Oe.flatMap(s=>s.items),Yn=Oe.flatMap(s=>s.items.filter(n=>!n.path.includes(":")).map(n=>({...n,group:s.label??""})));class Is extends Error{constructor(n,t){super(n),this.status=t,this.name="ApiError"}}const Jn=5*60*1e3;let yn=Date.now();for(const s of["pointerdown","pointermove","keydown","wheel","scroll"])window.addEventListener(s,()=>{yn=Date.now()},{passive:!0});const Qn=()=>Date.now()-yn({}));throw new Is(i.error??`Request failed (${t.status})`,t.status)}if(t.status!==204)return await t.json()}function Ve(s){const n=new URLSearchParams;for(const[i,a]of Object.entries(s))a==null||a===""||a===!1||n.set(i,String(a));const t=n.toString();return t?`?${t}`:""}const q={get:s=>Xe(s),post:(s,n)=>Xe(s,{method:"POST",body:n===void 0?void 0:JSON.stringify(n)}),put:(s,n)=>Xe(s,{method:"PUT",body:n===void 0?void 0:JSON.stringify(n)}),del:s=>Xe(s,{method:"DELETE"})},et=3e4,wn=v.createContext(null);function st({children:s}){var j;const[n,t]=v.useState(),[i,a]=v.useState(!1),[l,r]=v.useState(""),[d,c]=v.useState(""),[o,h]=v.useState(!0),x=v.useRef(0),u=v.useCallback(async()=>{const m=++x.current;try{const y=await q.get("/admin/api/status");if(m!==x.current)return;t(y),a(!0),c("")}catch(y){if(m!==x.current)return;a(!1),c(y instanceof Error?y.message:String(y))}finally{m===x.current&&(r(new Date().toISOString()),h(!1))}},[]),g=v.useCallback(async m=>{var y;await q.post("/admin/api/maintenance",{enabled:m,message:((y=n==null?void 0:n.maintenance)==null?void 0:y.message)??""}),await u()},[u,(j=n==null?void 0:n.maintenance)==null?void 0:j.message]);v.useEffect(()=>{u();let m;const y=()=>{window.clearInterval(m),m=document.hidden?void 0:window.setInterval(()=>void u(),et)},f=()=>{y(),document.hidden||u()};return y(),document.addEventListener("visibilitychange",f),()=>{window.clearInterval(m),document.removeEventListener("visibilitychange",f)}},[u]);const b=v.useMemo(()=>{var m;return{status:n,version:(n==null?void 0:n.serverVersion)??"",currentUser:((m=n==null?void 0:n.currentUser)==null?void 0:m.trim())||"Administrator",online:i,checkedAt:l,error:d,loading:o,reload:u,setMaintenance:g}},[n,i,l,d,o,u,g]);return e.jsx(wn.Provider,{value:b,children:s})}function je(){const s=v.useContext(wn);if(!s)throw new Error("useGateway used outside GatewayProvider");return s}function nt(s,n){const t=s.label.toLowerCase(),i=s.group.toLowerCase();return t.startsWith(n)?4:t.includes(n)?3:i.includes(n)?2:`${s.title} ${s.intro}`.toLowerCase().includes(n)?1:0}function tt(){const s=ws(),{status:n}=je(),[t,i]=v.useState(!1),[a,l]=v.useState(""),[r,d]=v.useState(0),c=v.useRef(null),o=v.useRef(null),h=v.useMemo(()=>{const u=a.trim().toLowerCase();return[...Yn,...((n==null?void 0:n.requestUsers)??[]).map(b=>({id:`user-${b.id}`,path:`/admin/accounts/${encodeURIComponent(b.id)}`,label:b.username||"Unnamed user",title:`User: ${b.username||"Unnamed user"}`,intro:"Open this user’s devices and settings.",group:"Users",icon:"people"})),...((n==null?void 0:n.clients)??[]).map(b=>({id:`device-${b.deviceId}`,path:`/admin/devices/${encodeURIComponent(b.deviceId)}`,label:b.deviceName||"Unnamed device",title:`Device: ${b.deviceName||"Unnamed device"}`,intro:`${b.username||"Unknown user"} · ${b.version||"unknown version"}`,group:"Devices",icon:"tv"}))].map((b,j)=>({item:b,rank:u?nt(b,u):1,index:j})).filter(b=>b.rank>0).sort((b,j)=>j.rank-b.rank||b.index-j.index).map(({item:b,rank:j})=>({item:b,rank:j}))},[a,n]);v.useEffect(()=>d(0),[a]),v.useEffect(()=>{const u=g=>{var b;(b=c.current)!=null&&b.contains(g.target)||i(!1)};return document.addEventListener("pointerdown",u),()=>document.removeEventListener("pointerdown",u)},[]),v.useEffect(()=>{const u=g=>{var j,m;if(g.key!=="S"||!g.shiftKey||g.ctrlKey||g.metaKey||g.altKey)return;const b=document.activeElement;b&&(b.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(b.tagName))||(g.preventDefault(),(j=o.current)==null||j.focus(),(m=o.current)==null||m.select())};return window.addEventListener("keydown",u),()=>window.removeEventListener("keydown",u)},[]);const x=u=>{var g;i(!1),l(""),(g=o.current)==null||g.blur(),s(u)};return e.jsxs("div",{className:"omni",ref:c,"data-open":t||void 0,children:[e.jsxs("div",{className:"omni-input",children:[e.jsx(se,{name:"search"}),e.jsx("input",{ref:o,type:"search",value:a,placeholder:"Search","aria-label":"Search","aria-expanded":t,onFocus:()=>i(!0),onChange:u=>{l(u.target.value),i(!0)},onKeyDown:u=>{var g;if(u.key==="Escape")l(""),i(!1),(g=o.current)==null||g.blur();else if(u.key==="ArrowDown")u.preventDefault(),d(b=>Math.min(b+1,h.length-1));else if(u.key==="ArrowUp")u.preventDefault(),d(b=>Math.max(b-1,0));else if(u.key==="Enter"){const b=h[r];if(!b)return;u.preventDefault(),x(b.item.path)}}}),e.jsx("span",{className:"omni-key",children:"⇧S"})]}),t?e.jsx("div",{className:"omni-panel",role:"listbox",children:h.length===0?e.jsx("p",{className:"empty",children:"No pages, users or devices match that search."}):h.map((u,g)=>e.jsxs("a",{className:g===r?"omni-item on":"omni-item",href:u.item.path,role:"option","aria-selected":g===r,onPointerEnter:()=>d(g),onClick:b=>{b.preventDefault(),x(u.item.path)},children:[u.item.icon?e.jsx(se,{name:u.item.icon}):null,e.jsxs("span",{children:[e.jsx("b",{children:u.item.label}),e.jsx("small",{children:u.item.intro})]}),e.jsx("span",{className:"omni-group",children:u.item.group})]},u.item.id))}):null]})}const w=s=>(s??0).toLocaleString(),F=s=>s?new Date(s).toLocaleString():"—";function ye(s){if(!s)return"0s";if(s<1e3)return`${Math.round(s)}ms`;const n=Math.round(s/1e3);if(n<60)return`${n}s`;const t=Math.floor(n/60);return t<60?`${t}m ${n%60}s`:`${Math.floor(t/60)}h ${t%60}m`}function _e(s){const n=Math.round(Math.max(0,s??0)/6e4);if(n<=0)return"none";if(n<60)return`${n} min`;const t=Math.floor(n/60),i=n%60;return i===0?t===1?"1 hour":`${t} hours`:`${t}h ${i}m`}function Ne(s){if(!s||s<=0)return"on request only";if(s<60)return`every ${s}s`;const n=Math.round(s/60);if(n<60)return`every ${n} min`;const t=Math.round(n/60);return t<48?t===1?"hourly":`every ${t} hours`:`every ${Math.round(t/24)} days`}function he(s){const n=["B","KB","MB","GB"];let t=Number(s??0),i=0;for(;t>=1024&&iString(s??"?").trim().split(/\s+/).slice(0,2).map(n=>n[0]??"").join("").toUpperCase(),is=s=>`${Math.round((s??0)*100)}%`;function fe(s){if(!s)return"—";const n=Date.now()-new Date(s).getTime();if(n<0)return"just now";const t=Math.floor(n/1e3);if(t<45)return"just now";const i=Math.floor(t/60);if(i<60)return`${i} min ago`;const a=Math.floor(i/60);if(a<24)return`${a}h ago`;const l=Math.floor(a/24);return l<30?`${l}d ago`:new Date(s).toLocaleDateString()}function ds(s){if(!s)return"—";const n=new Date(s).getTime()-Date.now();if(!Number.isFinite(n))return"—";if(n<=0)return"due now";const t=Math.round(n/1e3);if(t<60)return`in ${t}s`;const i=Math.round(t/60);if(i<60)return`in ${i} min`;const a=Math.round(i/60);return a<24?`in ${a}h`:`in ${Math.round(a/24)}d`}const Nn=15*60*1e3,at=3*60*60*1e3,Ye=s=>!!s&&Date.now()-new Date(s).getTime(){var y;try{const f=await q.get(`/admin/api/notifications?limit=${Ds}`);t(f.events),a(f.unread),r(f.types),x.current=Math.max(x.current,((y=f.events[0])==null?void 0:y.id)??0),h("")}catch(f){h(f instanceof Error?f.message:String(f))}},[]),g=v.useCallback(y=>{x.current=Math.max(x.current,y.id),t(f=>f.some(p=>p.id===y.id)?f:[y,...f].sort((p,R)=>R.id-p.id).slice(0,Ds)),y.readAt||a(f=>f+1),r(f=>f.some(p=>p.type===y.type)?f.map(p=>p.type===y.type?{...p,count:p.count+1}:p):[...f,{type:y.type,count:1}])},[]);v.useEffect(()=>{u()},[u]),v.useEffect(()=>{let y=null,f,p=!1;return(()=>{p||(y=new EventSource(`/admin/api/notifications/stream?after=${x.current}`),y.addEventListener("open",()=>{c(!0),window.clearInterval(f),f=void 0}),y.addEventListener("admin",k=>{try{g(JSON.parse(k.data))}catch{}}),y.addEventListener("error",()=>{c(!1),f===void 0&&(f=window.setInterval(()=>void u(),it))}))})(),()=>{p=!0,y==null||y.close(),window.clearInterval(f)}},[g,u]);const b=v.useCallback(async y=>{const f=y.filter(p=>p>0);if(f.length!==0){t(p=>p.map(R=>f.includes(R.id)&&!R.readAt?{...R,readAt:new Date().toISOString()}:R));try{const p=await q.post("/admin/api/notifications/read",{ids:f});a(p.unread)}catch{u()}}},[u]),j=v.useCallback(async()=>{a(0),t(y=>y.map(f=>f.readAt?f:{...f,readAt:new Date().toISOString()}));try{const y=await q.post("/admin/api/notifications/read",{all:!0});a(y.unread)}catch{u()}},[u]),m=v.useMemo(()=>({events:n,unread:i,types:l,connected:d,error:o,markRead:b,markAllRead:j,reload:u}),[n,i,l,d,o,b,j,u]);return e.jsx(Sn.Provider,{value:m,children:s})}function Ss(){const s=v.useContext(Sn);if(!s)throw new Error("useNotifications used outside NotificationProvider");return s}function Cn(s){return s.severity==="error"?"bad":s.severity==="warning"?"warn":"info"}function Mn(s){return s.startsWith("auth.")?"key":s.startsWith("device.")?"tv":s.startsWith("admin.")?"shield":s.startsWith("task.")?"clock":s.startsWith("integration.")?"plug":s.startsWith("library.")?"library":s.startsWith("emby.")?"globe":s.startsWith("server.")?"power":"bell"}function qs(s){return{"auth.login":"Signed in","auth.login_failed":"Sign-in refused","auth.logout":"Signed out","device.registered":"New device","device.removed":"Device removed","device.renamed":"Device renamed","admin.sign_in":"Admin sign-in","server.started":"Server started","server.maintenance":"Maintenance","task.completed":"Task finished","task.failed":"Task failed","integration.failed":"Integration failed","integration.test":"Integration test","library.sync":"Library sync","emby.unreachable":"Emby unreachable","emby.recovered":"Emby recovered"}[s]??s.replace(/[._]/g," ")}const Fs=99;function lt(){const{events:s,unread:n,connected:t,markRead:i,markAllRead:a}=Ss(),[l,r]=v.useState(!1),d=v.useRef(null);return v.useEffect(()=>{const c=h=>{var x;(x=d.current)!=null&&x.contains(h.target)||r(!1)},o=h=>{h.key==="Escape"&&r(!1)};return document.addEventListener("pointerdown",c),document.addEventListener("keydown",o),()=>{document.removeEventListener("pointerdown",c),document.removeEventListener("keydown",o)}},[]),v.useEffect(()=>{if(!l)return;const c=s.filter(o=>!o.readAt).map(o=>o.id);c.length>0&&i(c)},[l]),e.jsxs("div",{className:"bell",ref:d,"data-open":l||void 0,children:[e.jsxs("button",{type:"button",className:"bell-button","aria-label":n>0?`Activity, ${n} unread`:"Activity","aria-expanded":l,onClick:()=>r(c=>!c),children:[e.jsx(se,{name:"bell"}),n>0?e.jsx("span",{className:"bell-badge",children:n>Fs?`${Fs}+`:n}):null]}),l?e.jsxs("div",{className:"bell-panel",children:[e.jsxs("div",{className:"bell-head",children:[e.jsx("b",{children:"Activity"}),e.jsxs("div",{className:"row tight",children:[t?null:e.jsx("span",{className:"tag","data-tone":"warn",children:"reconnecting"}),n>0?e.jsx("button",{type:"button","data-variant":"quiet","data-size":"sm",onClick:()=>void a(),children:"Mark all read"}):null]})]}),e.jsx("div",{className:"bell-list",children:s.length===0?e.jsx("p",{className:"empty",children:"Nothing has happened yet."}):s.slice(0,20).map(c=>{const o=e.jsxs(e.Fragment,{children:[e.jsx(Qe,{name:Mn(c.type),tone:Cn(c)}),e.jsxs("span",{className:"bell-body",children:[e.jsx("b",{children:c.title||c.type}),c.summary?e.jsx("p",{children:c.summary}):null,e.jsx("time",{dateTime:c.occurredAt,children:fe(c.occurredAt)})]})]});return c.link?e.jsx(Z,{className:"bell-item","data-unread":!c.readAt||void 0,to:c.link,onClick:()=>r(!1),children:o},c.id):e.jsx("div",{className:"bell-item","data-unread":!c.readAt||void 0,children:o},c.id)})}),e.jsx("div",{className:"bell-foot",children:e.jsx(Z,{to:"/admin/activity",onClick:()=>r(!1),children:"All activity"})})]}):null]})}function J(s,n={}){const{pollMs:t,enabled:i=!0}=n,[a,l]=v.useState(),[r,d]=v.useState(""),[c,o]=v.useState(i),[h,x]=v.useState(!1),u=v.useRef(0),g=v.useRef(!1),b=v.useCallback(async()=>{if(!i)return;const j=++u.current;g.current&&x(!0);try{const m=await q.get(s);if(j!==u.current)return;l(m),d(""),g.current=!0}catch(m){if(j!==u.current)return;d(m instanceof Error?m.message:String(m))}finally{j===u.current&&(o(!1),x(!1))}},[s,i]);return v.useEffect(()=>(g.current=!1,o(!0),b(),()=>{u.current+=1}),[b]),v.useEffect(()=>{if(!t||!i)return;let j;const m=()=>{window.clearInterval(j),j=document.hidden?void 0:window.setInterval(()=>void b(),t)},y=()=>{m(),document.hidden||b()};return m(),document.addEventListener("visibilitychange",y),()=>{window.clearInterval(j),document.removeEventListener("visibilitychange",y)}},[t,i,b]),{data:a,error:r,loading:c,refreshing:h,reload:b,set:l}}function ne(){const[s,n]=v.useState(null),t=v.useRef(!0);v.useEffect(()=>()=>{t.current=!1},[]);const i=v.useCallback(async(a,l)=>{n(a);try{return await l(),!0}finally{t.current&&n(null)}},[]);return{busy:s,run:i}}const Os="Memby";function ot(s){v.useEffect(()=>{const n=s.trim();document.title=n?`${n} | ${Os}`:Os},[s])}function U({title:s,intro:n,actions:t,crumbs:i,icon:a}){var d;const l=ks(),r=a??((d=Zn.find(c=>vs({path:c.path,end:!0},l.pathname)))==null?void 0:d.icon);return ot(s),e.jsxs("header",{className:"page-head",children:[i?e.jsx("nav",{className:"crumbs",children:i}):null,e.jsxs("div",{className:"page-head-row",children:[e.jsxs("div",{className:"page-head-title",children:[r?e.jsx("span",{className:"page-head-icon","aria-hidden":"true",children:e.jsx(se,{name:r})}):null,e.jsxs("div",{className:"page-head-text",children:[e.jsx("h1",{children:s}),n?e.jsx("p",{children:n}):null]})]}),t?e.jsx("div",{className:"page-head-actions",children:t}):null]})]})}function M({title:s,intro:n,icon:t,tone:i,actions:a,footer:l,children:r}){return e.jsxs("section",{className:"card",children:[s?e.jsxs("div",{className:"card-head",children:[t?e.jsx(Qe,{name:t,tone:i}):null,e.jsxs("div",{className:"card-head-text",children:[e.jsx("h2",{children:s}),n?e.jsx("p",{children:n}):null]}),a?e.jsx("div",{className:"card-head-actions",children:a}):null]}):null,r,l?e.jsx("div",{className:"card-foot",children:l}):null]})}function xe({cols:s,children:n}){return e.jsx("div",{className:"grid","data-cols":s,children:n})}function ae({tiles:s}){return e.jsx("div",{className:"tiles",children:s.map(n=>e.jsxs("div",{className:"tile",children:[n.icon?e.jsx(Qe,{name:n.icon,tone:n.tone}):null,e.jsx("b",{className:n.small?"small":void 0,children:n.value}),e.jsx("span",{children:n.label})]},n.label))})}function N({children:s,tone:n}){return e.jsx("span",{className:"tag","data-tone":n,children:s})}function oe({children:s,tone:n}){return e.jsx("span",{className:"chip","data-tone":n,children:s})}function ee({children:s}){return e.jsx("p",{className:"empty",children:s})}function Y({columns:s,children:n}){return e.jsx("tr",{children:e.jsx("td",{colSpan:s,className:"muted",children:e.jsx("p",{className:"empty",children:n})})})}function pe({children:s,tone:n}){return e.jsx("p",{className:"note","data-tone":n,children:s})}function T({children:s,onClick:n,variant:t,size:i,disabled:a,busy:l,icon:r,type:d="button",title:c}){return e.jsxs("button",{type:d,className:"","data-variant":t,"data-size":i,disabled:a||l,onClick:n,title:c,children:[l?e.jsx("span",{className:"spinner"}):r?e.jsx(se,{name:r}):null,s]})}function D({label:s,hint:n,children:t,grow:i}){return e.jsxs("label",{className:i?"field grow":"field",children:[e.jsx("span",{children:s}),t,n?e.jsx("small",{children:n}):null]})}function G({label:s,hint:n,checked:t,onChange:i,disabled:a}){return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:t,disabled:a,onChange:l=>i(l.target.checked)}),e.jsx("span",{className:"switch"}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:s}),n?e.jsx("p",{children:n}):null]})]})}function Ue({value:s,options:n,onChange:t}){return e.jsx("div",{className:"segments",role:"group",children:n.map(i=>e.jsx("button",{type:"button","aria-pressed":i.value===s,onClick:()=>t(i.value),children:i.label},String(i.value)))})}function K({children:s}){return e.jsx("div",{className:"table-wrap",children:s})}function hs({data:s,labelOf:n,valueOf:t,toneOf:i,title:a}){if(s.length===0)return e.jsx(ee,{children:"Nothing in this window."});const l=s.map(d=>t(d)),r=Math.max(1,...l);return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"bars",children:s.map((d,c)=>{const o=t(d);return e.jsx("div",{className:"bar","data-tone":i==null?void 0:i(d),"data-empty":o===0||void 0,style:{height:`${Math.max(2,o/r*100)}%`},title:a?a(d):`${n(d,c)}: ${o}`},c)})}),e.jsxs("div",{className:"bars-axis",children:[e.jsx("span",{children:n(s[0],0)}),e.jsx("span",{children:n(s[s.length-1],s.length-1)})]})]})}function Tn({value:s,total:n,tone:t}){const i=n>0?Math.min(1,s/n):0;return e.jsx("div",{className:"meter","data-tone":t,children:e.jsx("div",{style:{width:`${i*100}%`}})})}function _({message:s,onDismiss:n}){return s?e.jsxs("div",{className:"banner",role:"alert",children:[e.jsx(se,{name:"alert"}),e.jsx("span",{children:s}),n?e.jsx("button",{type:"button",onClick:n,"aria-label":"Dismiss",children:e.jsx(se,{name:"close"})}):null]}):null}function V({rows:s=3}){return e.jsxs("div",{className:"loading-page","aria-busy":"true","aria-label":"Loading",children:[e.jsxs("div",{className:"loading-heading",children:[e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"})]}),e.jsx("div",{className:"loading-tiles",children:Array.from({length:4},(n,t)=>e.jsx("span",{className:"skeleton"},t))}),Array.from({length:s},(n,t)=>e.jsxs("section",{className:"loading-card",children:[e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"})]},t))]})}function ke({title:s,body:n,confirmLabel:t="Confirm",destructive:i,busy:a,onConfirm:l,onCancel:r}){const d=v.useId(),c=v.useRef(null);return v.useEffect(()=>{var h;(h=c.current)==null||h.focus();const o=x=>{x.key==="Escape"&&r()};return document.addEventListener("keydown",o),()=>document.removeEventListener("keydown",o)},[r]),e.jsx("div",{className:"scrim",onPointerDown:o=>o.target===o.currentTarget&&r(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true","aria-labelledby":d,ref:c,tabIndex:-1,children:[e.jsx("h2",{id:d,children:s}),e.jsx("p",{children:n}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{onClick:r,variant:"quiet",children:"Cancel"}),e.jsx(T,{onClick:l,variant:i?"danger":"primary",busy:a,children:t})]})]})})}function We({rows:s}){return e.jsx("div",{className:"kv",children:s.map(n=>e.jsxs("div",{className:"kv-row",children:[e.jsx("span",{children:n.label}),e.jsx("span",{children:n.value})]},n.label))})}function xs({children:s,aside:n}){return e.jsxs("h4",{className:"subhead",children:[e.jsx("span",{children:s}),n?e.jsx("small",{children:n}):null]})}function Cs({tiles:s}){return e.jsx("div",{className:"tiles plain",children:s.map(n=>e.jsxs("div",{className:"tile",children:[e.jsx("b",{className:n.small?"small":void 0,children:n.value}),e.jsx("span",{children:n.label})]},n.label))})}function ct({open:s,onNavigate:n}){const{unread:t}=Ss(),i=ks(),[a,l]=v.useState(()=>{try{return JSON.parse(localStorage.getItem("memby-admin-nav")??"{}")}catch{return{}}}),r=c=>{try{localStorage.setItem("memby-admin-nav",JSON.stringify(c))}catch{}};v.useEffect(()=>{const c=Oe.find(o=>o.items.some(h=>vs({path:h.path,end:!0},i.pathname)));c&&l(o=>{const h={...o};return Oe.forEach(x=>{x.collapsible!==!1&&(h[x.id]=x.id!==c.id)}),r(h),h})},[i.pathname]);const d=(c,o=!1)=>{l(h=>{const x={...h},u=!(h[c]??o);return Oe.forEach(g=>{g.collapsible!==!1&&(x[g.id]=g.id===c?u:!0)}),r(x),x})};return e.jsx("nav",{className:"rail",id:"rail","data-open":s||void 0,"aria-label":"Console sections",children:Oe.map(c=>{const o=c.items.filter(g=>!g.hidden);if(o.length===0)return null;const h=o.some(g=>vs({path:g.path,end:!0},i.pathname)),x=c.collapsible!==!1,u=h||!x||!(a[c.id]??c.defaultCollapsed??!1);return e.jsxs("div",{className:"rail-group",children:[c.label&&x?e.jsxs("button",{type:"button",className:"rail-head","aria-expanded":u,onClick:()=>d(c.id,c.defaultCollapsed),children:[c.label,e.jsx(se,{name:"caret",className:"ico caret"})]}):c.label?e.jsx("div",{className:"rail-head rail-head-static",children:c.label}):null,u?o.map(g=>e.jsx(jn,{to:g.path,end:g.path==="/admin",onClick:n,className:({isActive:b})=>b?"on":"","aria-current":void 0,children:({isActive:b})=>e.jsxs("span",{style:{display:"contents"},ref:j=>{const m=j==null?void 0:j.parentElement;m&&(b?m.setAttribute("aria-current","page"):m.removeAttribute("aria-current"))},children:[g.icon?e.jsx(se,{name:g.icon}):null,g.label,g.badge==="notifications"&&t>0?e.jsx("span",{className:"rail-badge",children:t>99?"99+":t}):null]})},g.id)):null]},c.id)})})}function dt(){var k,$,I;const{version:s,currentUser:n,online:t,loading:i,status:a,setMaintenance:l}=je(),[r,d]=v.useState(!1),[c,o]=v.useState(!1),[h,x]=v.useState(!1),[u,g]=v.useState(!1),b=ks(),j=!!((k=a==null?void 0:a.maintenance)!=null&&k.enabled),m=!!(($=a==null?void 0:a.quietTime)!=null&&$.active),y=v.useRef(null),f=((I=Array.from(n.trim())[0])==null?void 0:I.toLocaleUpperCase("en-NZ"))||"A",p=a&&t&&!j&&!m?"ok":a||!i?"bad":"checking",R=async()=>{if(!(h||!a)){x(!0);try{await l(!j)}finally{x(!1)}}};return v.useEffect(()=>d(!1),[b.pathname]),v.useEffect(()=>{if(!c)return;const A=X=>{var le;(le=y.current)!=null&&le.contains(X.target)||o(!1)},L=X=>{X.key==="Escape"&&o(!1)};return document.addEventListener("mousedown",A),window.addEventListener("keydown",L),()=>{document.removeEventListener("mousedown",A),window.removeEventListener("keydown",L)}},[c]),v.useEffect(()=>{if(!r)return;const A=document.body.style.overflow;document.body.style.overflow="hidden";const L=X=>{X.key==="Escape"&&d(!1)};return window.addEventListener("keydown",L),()=>{document.body.style.overflow=A,window.removeEventListener("keydown",L)}},[r]),e.jsxs(e.Fragment,{children:[e.jsx("a",{className:"skip",href:"#main",children:"Skip to content"}),e.jsxs("header",{className:"topbar",children:[e.jsxs("a",{className:"topbar-brand",href:"/admin",children:[e.jsx("span",{className:"brand-mark","aria-hidden":"true",children:"M"}),e.jsx("span",{className:"brand-word",children:"Memby Gateway"})]}),e.jsx("button",{type:"button",className:"rail-toggle","aria-label":"Sections","aria-expanded":r,"aria-controls":"rail",onClick:()=>d(A=>!A),children:e.jsx(se,{name:"menu"})}),e.jsx("div",{className:"topbar-spacer"}),e.jsxs("div",{className:"topbar-tools",children:[e.jsx(tt,{}),e.jsxs("span",{className:"topbar-version",children:["gateway ",s||"unknown"]}),e.jsx("button",{type:"button",className:"topbar-status","data-tone":p,"aria-pressed":j,disabled:!a||!t||h||m,title:a?m?"Memby quiet time is active":j?"Bring Memby back online":t?"Take Memby offline":"Memby is not responding":"Checking Memby status","aria-label":m?"Memby quiet time is active":j?"Memby is offline. Bring it online":a&&t?"Memby is online. Take it offline":i?"Checking Memby status":"Memby is not responding",onClick:()=>j?void R():g(!0),children:e.jsx("span",{className:"dot","aria-hidden":"true"})}),e.jsx(lt,{}),e.jsxs("div",{className:"account-menu","data-open":c||void 0,ref:y,children:[e.jsxs("button",{type:"button",className:"account-trigger","aria-haspopup":"menu","aria-expanded":c,"aria-label":`Signed in as ${n}`,onClick:()=>o(A=>!A),children:[e.jsx("span",{className:"account-avatar","aria-hidden":"true",children:f}),e.jsx("span",{className:"account-name",children:n}),e.jsx(se,{name:"caret",className:"ico account-caret"})]}),c?e.jsxs("div",{className:"account-panel",role:"menu",children:[e.jsxs("div",{className:"account-identity",children:[e.jsx("span",{className:"account-avatar account-avatar-large","aria-hidden":"true",children:f}),e.jsxs("span",{children:[e.jsx("small",{children:"Signed in as"}),e.jsx("b",{children:n})]})]}),e.jsxs(jn,{to:"/admin/settings",role:"menuitem",onClick:()=>o(!1),children:[e.jsx(se,{name:"sliders"}),"Gateway settings"]}),e.jsx("form",{method:"post",action:"/admin/logout",children:e.jsxs("button",{type:"submit",role:"menuitem",children:[e.jsx(se,{name:"logout"}),"Log out"]})})]}):null]})]})]}),r?e.jsx("button",{type:"button",className:"rail-scrim","aria-label":"Close sections",onClick:()=>d(!1)}):null,e.jsx(ct,{open:r,onNavigate:()=>d(!1)}),e.jsx("main",{className:"page",id:"main",children:e.jsx(_n,{})}),u?e.jsx(ke,{title:"Take Memby offline?",body:"Every television will stop working immediately. Viewers will see the maintenance message configured on the Maintenance page, while this console remains available.",confirmLabel:"Go offline",destructive:!0,busy:h,onConfirm:()=>{g(!1),R()},onCancel:()=>g(!1)}):null]})}const Rn=v.createContext(null),ht=5e3;function ut({children:s}){const[n,t]=v.useState([]),i=v.useRef(1),a=v.useCallback(c=>{t(o=>o.filter(h=>h.id!==c))},[]),l=v.useCallback((c,o="ok")=>{const h=i.current++;t(x=>[...x,{id:h,message:c,tone:o}]),window.setTimeout(()=>a(h),ht)},[a]),r=v.useCallback(async(c,o)=>{try{const h=await c();return o&&l(o,"ok"),h}catch(h){l(h instanceof Error?h.message:String(h),"bad");return}},[l]),d=v.useMemo(()=>({show:l,wrap:r}),[l,r]);return e.jsxs(Rn.Provider,{value:d,children:[s,e.jsx("div",{className:"toasts",role:"status","aria-live":"polite",children:n.map(c=>e.jsxs("div",{className:"toast","data-tone":c.tone,children:[e.jsx(se,{name:c.tone==="bad"?"alert":"check"}),e.jsx("span",{children:c.message}),e.jsx("button",{type:"button",onClick:()=>a(c.id),"aria-label":"Dismiss",children:e.jsx(se,{name:"close"})})]},c.id))})]})}function ie(){const s=v.useContext(Rn);if(!s)throw new Error("useToast used outside ToastProvider");return s}const Ps={ok:"ok",watch:"warn",bad:"bad"},mt={ok:"Healthy",watch:"Worth watching",bad:"Needs attention"};function $n({health:s,compact:n}){const t=s.notes??[];return e.jsxs("div",{className:"verdict","data-tone":Ps[s.level]??"ok",children:[e.jsxs("div",{className:"verdict-head",children:[e.jsx(se,{name:s.level==="ok"?"check":"alert"}),e.jsx("b",{children:mt[s.level]??"Unknown"}),e.jsx("span",{children:s.summary})]}),n||t.length===0?null:e.jsx("ul",{className:"verdict-notes",children:t.map((i,a)=>e.jsxs("li",{children:[e.jsx(N,{tone:Ps[i.level]??"note",children:i.area}),e.jsx("span",{children:i.message})]},`${i.area}-${a}`))})]})}function rs(s,n){switch(s.direction){case"rising":return`rising ${n(Math.abs(s.perHour))} an hour`;case"falling":return`falling ${n(Math.abs(s.perHour))} an hour`;case"steady":return"steady";default:return"gathering history"}}function En(s){if(!s||s<60)return`${Math.max(0,Math.round(s))}s`;const n=Math.floor(s/60);if(n<60)return`${n} min`;const t=Math.floor(n/60);return t<48?`${t}h ${n%60}m`:`${Math.floor(t/24)} days`}function pt(){var y,f,p,R,k,$;const{status:s,error:n,loading:t}=je(),i=J("/admin/api/runtime",{pollMs:3e4}),a=J("/admin/api/views",{pollMs:6e4});if(t||!s)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Overview",intro:"What the gateway is doing right now."}),e.jsx(_,{message:n}),e.jsx(V,{})]});const l=s.features??{features:[],revision:0,safeMode:!1},r=l.features??[],d=s.clients??[],c=d.filter(I=>Ye(I.lastSeen)).length,o=s.updatePolicy??{},h=!!o.minimumVersion&&o.minimumVersion===o.latestVersion,x=s.playbackPolicy,u=s.mdblist,g=s.forYou,b=(s.runs??[]).slice(0,5),j=i.data,m=j?j.memory.memoryLimit>0&&j.memory.memoryLimitI.enabled).length} / ${r.length}`,icon:"sliders",tone:"ok"},{label:"last import",value:F(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"What televisions are being told",intro:"The answers the gateway is giving every set right now.",icon:"tv",tone:"info",children:e.jsx(We,{rows:[{label:"Availability",value:(k=s.maintenance)!=null&&k.enabled?e.jsx(N,{tone:"bad",children:"offline for maintenance"}):($=s.quietTime)!=null&&$.active?e.jsx(N,{tone:"warn",children:"quiet time active"}):e.jsx(N,{tone:"ok",children:"online"})},{label:"Feature control plane",value:l.safeMode?e.jsx(N,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(N,{tone:"ok",children:["revision r",w(l.revision)]})},{label:"App update prompt",value:o.enabled?e.jsxs(N,{tone:h?"warn":"ok",children:[h?"required · ":"optional · ",o.latestVersion]}):e.jsx(N,{children:"off"})},{label:"Catalogue import",value:s.syncRunning?e.jsx(N,{tone:"warn",children:"running"}):e.jsxs(N,{children:["every ",s.syncEvery]})},{label:"Playback preroll",value:(x==null?void 0:x.prerollEnabled)===!1?e.jsx(N,{children:"off"}):e.jsxs(N,{tone:"ok",children:[((x==null?void 0:x.prerollDurationMs)??6500)/1e3,"s"]})}]})}),e.jsx(M,{title:"Services",intro:"The services this gateway leans on, and whether they answered.",icon:"wrench",tone:"note",actions:e.jsx(Z,{to:"/admin/integrations",children:"Integrations"}),children:e.jsx(We,{rows:[{label:"Movies (Radarr)",value:s.radarrReady?e.jsx(N,{tone:"ok",children:"ready"}):e.jsx(N,{children:"not configured"})},{label:"Series (Sonarr)",value:s.sonarrReady?e.jsx(N,{tone:"ok",children:"ready"}):e.jsx(N,{children:"not configured"})},{label:"MDBList ratings",value:u!=null&&u.enabled?e.jsxs(N,{tone:"ok",children:[w(u.cachedTitles)," titles stored"]}):e.jsx(N,{children:u!=null&&u.apiKeyConfigured?"off · key saved":"off · no key"})},{label:"For You pools",value:s.forYouRunning?e.jsx(N,{tone:"warn",children:"rebuilding"}):e.jsxs(N,{children:[w((g==null?void 0:g.candidates)??0)," ranked candidates"]})},{label:"Recommendation profiles",value:e.jsx("span",{className:"mono",children:w((g==null?void 0:g.profiles)??0)})}]})})]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"Latest imports",intro:"The last few catalogue synchronisations.",icon:"sync",tone:"data",actions:e.jsx(Z,{to:"/admin/imports",children:"All imports"}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Started"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"num",children:"Written"})]})}),e.jsx("tbody",{children:b.length===0?e.jsx(Y,{columns:4,children:"No imports have run yet."}):b.map(I=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(I.startedAt)}),e.jsx("td",{children:I.kind}),e.jsx("td",{children:e.jsx(N,{tone:I.status==="success"?"ok":I.status==="running"?"warn":"bad",children:I.status})}),e.jsx("td",{className:"num",children:w(I.itemsUpserted)})]},I.id||I.startedAt))})]})})}),e.jsx(M,{title:"Process",intro:"The container the gateway is served from.",icon:"chip",tone:"info",actions:e.jsx(Z,{to:"/admin/runtime",children:e.jsx(T,{size:"sm",variant:"quiet",icon:"external",children:"Runtime details"})}),children:j?e.jsxs(e.Fragment,{children:[e.jsx($n,{health:j.health,compact:!0}),e.jsx(Cs,{tiles:[{label:`heap used · ${rs(j.heapTrend,he)}`,value:he(j.memory.heapInuse)},{label:"of the process limit",value:m?`${(j.memory.heapShare*100).toFixed(1)}%`:"no limit"},{label:`goroutines · ${rs(j.goroutineTrend,I=>w(Math.round(I)))}`,value:w(j.goroutines)},{label:"running for",value:En(j.uptimeSeconds)}]}),e.jsxs("p",{className:"hint",children:["Heap in use is what the gateway is holding;"," ",he(j.memory.sys)," is reserved from the operating system on its behalf, which is always larger and is not a leak.",m?` The limit is ${he(j.memory.memoryLimit)}${j.memory.configuredLimit?" (GOMEMLIMIT)":""}.`:" No memory limit is set."]})]}):e.jsx("p",{className:"hint",children:"Reading process statistics…"})})]})]})}const xt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"}];function jt(){const{unread:s,connected:n,markAllRead:t,reload:i}=Ss(),[a,l]=v.useState(7),[r,d]=v.useState(""),[c,o]=v.useState(""),[h,x]=v.useState(!1),[u,g]=v.useState(0),b=100,j=v.useMemo(()=>`/admin/api/notifications${Ve({days:a,type:r,severity:c,unread:h,limit:b,offset:u*b})}`,[a,r,c,h,u]),{data:m,error:y,loading:f,reload:p}=J(j),R=async()=>{await t(),await p()};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Activity",intro:"Every administrative event the gateway has published: sign-ins, devices, scheduled tasks, integrations and the server itself. The same feed the bell and every integration read from.",actions:s>0?e.jsx(T,{onClick:()=>void R(),icon:"check",children:"Mark all read"}):void 0}),e.jsx(_,{message:y}),e.jsx(ae,{tiles:[{label:"Events in window",value:w((m==null?void 0:m.total)??0),icon:"bell",tone:"info"},{label:"Unread",value:w(s),icon:"alert",tone:s>0?"warn":void 0},{label:"Kinds seen",value:w((m==null?void 0:m.types.length)??0),icon:"list",tone:"note"},{label:"Live feed",value:n?"connected":"reconnecting",small:!0,icon:"pulse",tone:n?"ok":"warn"}]}),e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Ue,{value:a,options:xt.map(k=>({value:k.value,label:k.label})),onChange:k=>{l(k),g(0)}})}),e.jsx(D,{label:"Kind",children:e.jsxs("select",{value:r,onChange:k=>{d(k.target.value),g(0)},children:[e.jsx("option",{value:"",children:"Everything"}),((m==null?void 0:m.types)??[]).map(k=>e.jsxs("option",{value:k.type,children:[qs(k.type)," (",k.count,")"]},k.type))]})}),e.jsx(D,{label:"Severity",children:e.jsxs("select",{value:c,onChange:k=>{o(k.target.value),g(0)},children:[e.jsx("option",{value:"",children:"Any"}),e.jsx("option",{value:"info",children:"Information"}),e.jsx("option",{value:"warning",children:"Warning"}),e.jsx("option",{value:"error",children:"Error"})]})}),e.jsx(D,{label:"Read state",children:e.jsxs("select",{value:h?"unread":"",onChange:k=>{x(k.target.value==="unread"),g(0)},children:[e.jsx("option",{value:"",children:"All"}),e.jsx("option",{value:"unread",children:"Unread only"})]})}),e.jsx("div",{className:"filter-actions",children:e.jsx(T,{variant:"quiet",size:"sm",icon:"refresh",onClick:()=>{p(),i()},children:"Refresh"})})]}),f?e.jsx(V,{}):e.jsx(M,{title:"Events",icon:"bell",tone:"info",footer:((m==null?void 0:m.total)??0)>b?e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",disabled:u===0,onClick:()=>g(u-1),children:"Newer"}),e.jsx(T,{size:"sm",disabled:(u+1)*b>=((m==null?void 0:m.total)??0),onClick:()=>g(u+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"What happened"}),e.jsx("th",{children:"Who"}),e.jsx("th",{children:"What"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:((m==null?void 0:m.events.length)??0)===0?e.jsx(Y,{columns:6,children:"Nothing has happened in this window."}):m==null?void 0:m.events.map(k=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(k.occurredAt),children:fe(k.occurredAt)}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(Qe,{name:Mn(k.type),tone:Cn(k)}),qs(k.type)]})}),e.jsxs("td",{children:[e.jsx("b",{children:k.title}),k.summary?e.jsx("div",{className:"muted",children:k.summary}):null]}),e.jsx("td",{className:"muted nowrap",children:k.actor||"—"}),e.jsx("td",{className:"muted nowrap",children:k.target||"—"}),e.jsxs("td",{className:"nowrap",children:[k.readAt?null:e.jsx(N,{tone:"ok",children:"new"}),k.link?e.jsx(Z,{className:"table-row-link",to:k.link,children:"Open"}):null]})]},k.id))})]})})})]})}function _s(s){const n=s?new Date(s).getTime():0;return Number.isFinite(n)?n:0}function Us(){return e.jsx("span",{className:"muted",title:"No Tracearr sessions matched to this person",children:"—"})}function gt(){const{data:s,error:n,loading:t}=J("/admin/api/accounts",{pollMs:6e4}),i=(s==null?void 0:s.accounts)??[],a=i.flatMap(h=>h.devices??[]),l=i.filter(h=>{var x;return(x=h.recommendations)==null?void 0:x.completed}).length,r=i.filter(h=>{var x,u;return((x=h.recommendations)==null?void 0:x.prompted)&&!((u=h.recommendations)!=null&&u.completed)}).length,d=i.filter(h=>{var x;return(x=h.watchTime)==null?void 0:x.matched}),c=d.reduce((h,x)=>{var u;return h+(((u=x.watchTime)==null?void 0:u.weekMs)??0)},0),o=[...i].sort((h,x)=>_s(x.lastSeen)-_s(h.lastSeen));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Users",intro:"Who uses Memby, and which devices they are signed in to."}),e.jsx(_,{message:n}),t?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Memby users",value:w(i.length),icon:"people",tone:"note"},{label:"signed-in devices",value:w(a.length),icon:"tv",tone:"info"},{label:"active in the last 15 mins",value:w(a.filter(h=>Ye(h.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"recommendation setups completed",value:w(l),icon:"check",tone:"ok"},{label:"setup prompts queued",value:w(r),icon:"sparkle",tone:"note"},...d.length?[{label:"watch time this week",value:_e(c),icon:"pulse",tone:"data"}]:[]]}),e.jsx(M,{title:"Users",icon:"people",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"User"}),e.jsx("th",{children:"Short name"}),e.jsx("th",{className:"num",children:"Devices"}),e.jsx("th",{className:"num",children:"This week"}),e.jsx("th",{className:"num",children:"This month"}),e.jsx("th",{children:"Recommendations"}),e.jsx("th",{children:"Last seen"})]})}),e.jsx("tbody",{children:o.length===0?e.jsx(Y,{columns:7,children:"No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here."}):o.map(h=>{var m,y;const x=h.devices??[],u=x.filter(f=>Ye(f.lastSeen)).length,g=(m=h.recommendations)!=null&&m.completed?{label:"personalised",tone:"ok"}:(y=h.recommendations)!=null&&y.prompted?{label:"prompt queued",tone:"warn"}:{label:"not invited",tone:void 0},b=Ns(h.lastSeen),j=h.watchTime;return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":b.tone,title:b.label}),e.jsx("span",{className:"avatar",children:h.initials||kn(h.username)}),e.jsx(Z,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(h.id)}`,children:h.username||"Unnamed user"})]})}),e.jsx("td",{children:h.shortName||e.jsx("span",{className:"muted",title:"Memby greets them by their account name",children:"account name"})}),e.jsxs("td",{className:"num",children:[w(x.length),u?e.jsxs("span",{className:"table-sub",children:[w(u)," active now"]}):null]}),e.jsx("td",{className:"num",children:j!=null&&j.matched?_e(j.weekMs):e.jsx(Us,{})}),e.jsx("td",{className:"num muted",children:j!=null&&j.matched?_e(j.monthMs):e.jsx(Us,{})}),e.jsx("td",{children:e.jsx(N,{tone:g.tone,children:g.label})}),e.jsx("td",{className:"nowrap muted",title:F(h.lastSeen),children:fe(h.lastSeen)})]},h.id)})})]})})})]})]})}function js(s){const n=String(s??"").replace("#","");return n.length!==8?`#${n}`:`#${n.slice(2)}${n.slice(0,2)}`}function vt(){var z,ce,we,Re,$e,ge;const{userId:s=""}=Be(),n=ws(),{wrap:t}=ie(),{busy:i,run:a}=ne(),l=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:r,error:d,loading:c,reload:o}=J("/admin/api/accounts",{pollMs:3e4}),[h,x]=v.useState(null),[u,g]=v.useState(null),[b,j]=v.useState(null),[m,y]=v.useState(null),[f,p]=v.useState(null),[R,k]=v.useState(null),$=J(`${l}/viewers`),I=((r==null?void 0:r.accounts)??[]).find(E=>E.id===s),A=(((z=$.data)==null?void 0:z.viewers)??[]).filter(E=>E.kind!=="main"),L=!!((ce=$.data)!=null&&ce.enabled)&&A.length<(((we=$.data)==null?void 0:we.maxShadowViewers)??0),X=(r==null?void 0:r.catalogue)??[],le=(r==null?void 0:r.themes)??[];v.useEffect(()=>{var E;h===null&&I&&x({...((E=I.settings)==null?void 0:E.preferences)??{}})},[I,h]),v.useEffect(()=>{b===null&&I&&j({...I.notifications})},[I,b]),v.useEffect(()=>{if(u!==null||!I)return;const E=I.themes??[];g(E.length===0?le.map(P=>P.id):E)},[I,u,le]);const ue=v.useMemo(()=>{const E=[];for(const P of X){let de=E.find(me=>me.name===P.area);de||E.push(de={name:P.area,definitions:[]}),de.definitions.push(P)}return E},[X]),te=(E,P,de,me)=>a(E,async()=>{const Ie=await t(P,de);y(null),Ie!==void 0&&(me==null||me()),await o()});if(c)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(Z,{to:"/admin/accounts",children:"← All users"})}),e.jsx(V,{})]});if(!I)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(Z,{to:"/admin/accounts",children:"← All users"})}),e.jsx(_,{message:d}),e.jsx(M,{children:e.jsx(ee,{children:"This user is no longer signed in to Memby."})})]});const H=I.devices??[],re=H.filter(E=>Ye(E.lastSeen)).length,S=I.settings??{},O=I.recommendations??{};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:I.username||"Unnamed user",intro:`Memby user · ${I.shortName?`greeted as ${I.shortName} · `:""}${w(H.length)} device${H.length===1?"":"s"} · last seen ${F(I.lastSeen)}`,crumbs:e.jsx(Z,{to:"/admin/accounts",children:"← All users"}),actions:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"avatar",children:I.initials||kn(I.username)}),re?e.jsxs(N,{tone:"ok",children:[re," active now"]}):e.jsx(N,{children:"idle"}),e.jsx(oe,{children:I.id})]})}),e.jsx(_,{message:d}),e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Devices",intro:"Every build a set has been seen running is listed under it. Signing one out revokes its Memby session, drops that history and removes it from Emby's own device list. Its Emby account is not changed.",icon:"tv",tone:"info",children:H.length===0?e.jsx(ee,{children:"No devices are signed in to this user."}):e.jsx("div",{className:"list",children:H.map(E=>{const P=Ns(E.lastSeen);return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[e.jsx("span",{className:"dot-state","data-tone":P.tone,title:P.label})," ",e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(E.id)}`,children:E.name||"Memby TV"})]}),e.jsxs("p",{children:[E.version?`Memby ${E.version}`:"Legacy Memby client"," · ",P.label," · last seen ",F(E.lastSeen)," · signed in ",F(E.signedInAt)]}),(E.versions??[]).length>0?e.jsx("div",{className:"chips",children:(E.versions??[]).map(de=>e.jsxs(oe,{tone:de.version===E.version?"ok":void 0,children:[de.version,de.version===E.version?" · now":""]},de.version))}):null]}),e.jsxs("div",{className:"list-actions",children:[e.jsx(T,{size:"sm",disabled:!E.id,onClick:()=>p({id:E.id,name:E.name}),children:"Rename"}),e.jsx(T,{size:"sm",variant:"danger",disabled:!E.id,onClick:()=>y({kind:"remove-device",deviceId:E.id,name:E.name}),children:"Sign out"})]})]},E.id||E.name)})})}),e.jsxs(M,{title:"Viewers",intro:"Several people under one Emby account, each with their own Continue Watching, watched history and favourites. Only the account itself is synced with Emby; a shadow viewer's watching is kept by Memby and never reported.",icon:"people",tone:"note",actions:$.data&&!$.data.enabled?e.jsx(N,{tone:"warn",children:"switched off"}):e.jsxs(N,{tone:"ok",children:[w(A.length)," beside the account"]}),footer:L?e.jsx(T,{variant:"primary",onClick:()=>k({id:null,name:""}),children:"Add viewer"}):e.jsx(T,{disabled:!0,children:$.data?`${w($.data.maxShadowViewers)} is the limit`:"Add viewer"}),children:[$.data&&!$.data.enabled?e.jsx(_,{message:"Viewers are switched off for this server, so every television watches as the account. Nothing here is deleted — turn the feature on under Features to bring these people back."}):null,$.loading?e.jsx(V,{}):(((Re=$.data)==null?void 0:Re.viewers)??[]).length===0?e.jsx(ee,{children:"Nobody is set up yet, so this account has one viewer: itself."}):e.jsx("div",{className:"list",children:((($e=$.data)==null?void 0:$e.viewers)??[]).map(E=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:E.name}),e.jsxs("p",{children:[E.kind==="main"?"The account itself — named by Emby, and the only one whose watching Emby hears about":"Watches privately; nothing reaches Emby",E.createdAt&&E.kind!=="main"?` · added ${F(E.createdAt)}`:""]})]}),e.jsx("div",{className:"list-actions",children:E.kind==="main"?e.jsx(N,{tone:"info",children:"synced with Emby"}):e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",onClick:()=>k({id:E.id,name:E.name}),children:"Rename"}),e.jsx(T,{size:"sm",variant:"danger",onClick:()=>y({kind:"remove-viewer",viewer:E}),children:"Remove"})]})})]},E.id))})]}),e.jsxs(M,{title:"Recommendation setup",intro:"The prompt appears the next time this person opens Memby on any of their televisions.",icon:"sparkle",tone:"note",footer:O.completed?e.jsx(T,{busy:i==="reset-rec",onClick:()=>y({kind:"reset-recommendations"}),children:"Clear stored choices"}):O.prompted?e.jsx(T,{onClick:()=>y({kind:"cancel-prompt"}),children:"Cancel prompt"}):e.jsx(T,{variant:"primary",busy:i==="prompt",onClick:()=>void te("prompt",()=>q.put(`${l}/recommendations/prompt`),"Setup prompt queued."),children:"Send setup prompt"}),children:[e.jsx("div",{className:"row tight",children:O.completed?e.jsx(N,{tone:"ok",children:"completed"}):O.prompted?e.jsx(N,{tone:"warn",children:"prompt queued"}):e.jsx(N,{children:"not invited"})}),e.jsx(ft,{prompt:O})]})]}),(ge=I.watchTime)!=null&&ge.matched?e.jsx(M,{title:"Watch time",intro:"From Tracearr, for this person across every client — not only Memby. The week runs from Monday and the month from the first, both in the server's own timezone.",icon:"pulse",tone:"data",actions:I.watchTime.tracearrUsername?e.jsx(oe,{children:I.watchTime.tracearrUsername}):null,children:e.jsx(ae,{tiles:[{label:`this week · ${w(I.watchTime.weekSessions)} session${I.watchTime.weekSessions===1?"":"s"}`,value:_e(I.watchTime.weekMs),icon:"pulse",tone:"data"},{label:`this month · ${w(I.watchTime.monthSessions)} session${I.watchTime.monthSessions===1?"":"s"}`,value:_e(I.watchTime.monthMs),icon:"calendar",tone:"info"},{label:"since Tracearr started recording",value:_e(I.watchTime.totalMs),icon:"clock",tone:"note"},{label:"last watched",value:F(I.watchTime.lastWatchedAt),icon:"history",tone:void 0,small:!0}]})}):null,e.jsx(M,{title:"Notifications",intro:"Choose what this person sees across every television. Changes apply through the gateway within a few seconds and do not require an app release.",icon:"bell",tone:"note",actions:b!=null&&b.enabled?e.jsx(N,{tone:"ok",children:"enabled"}):e.jsx(N,{children:"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:i==="notifications",onClick:()=>void te("notifications",()=>q.put(`${l}/notifications`,b),"Notification settings saved.",()=>j(null)),children:"Save notifications"}),e.jsx(T,{onClick:()=>{j(null),o()},children:"Discard changes"})]}),children:b?e.jsxs("div",{className:"checks columns",children:[e.jsx(G,{label:"All notifications",hint:"The master switch. Turning this off hides every optional notification below.",checked:b.enabled,onChange:E=>j(P=>P&&{...P,enabled:E})}),e.jsx(G,{label:"My Shows return dates",hint:"Remind this person when a followed show is about to return.",checked:b.showReturnAlerts,disabled:!b.enabled,onChange:E=>j(P=>P&&{...P,showReturnAlerts:E})}),e.jsx(G,{label:"Sonarr television alerts",hint:"New episodes, additions and cancellation news supplied by Sonarr.",checked:b.sonarrAlerts,disabled:!b.enabled,onChange:E=>j(P=>P&&{...P,sonarrAlerts:E})}),e.jsx(G,{label:"Radarr film alerts",hint:"Notify this person when Radarr imports a new film.",checked:b.radarrAlerts,disabled:!b.enabled,onChange:E=>j(P=>P&&{...P,radarrAlerts:E})}),e.jsx(G,{label:"Optional app updates",hint:"Offer new app versions to this person. Mandatory compatibility updates are always enforced.",checked:b.updateAlerts,disabled:!b.enabled,onChange:E=>j(P=>P&&{...P,updateAlerts:E})}),e.jsx(G,{label:"Library activity",hint:"Show alerts after the Memby library catalogue is refreshed.",checked:b.libraryAlerts,disabled:!b.enabled,onChange:E=>j(P=>P&&{...P,libraryAlerts:E})}),e.jsx(G,{label:"Weekly watch-time summary",hint:"Send this person their week-to-date and month-to-date viewing on Sunday evening, and a summary of the month just gone once it ends. Needs Tracearr.",checked:b.watchTimeDigest,disabled:!b.enabled,onChange:E=>j(P=>P&&{...P,watchTimeDigest:E})}),e.jsx(G,{label:"Service status",hint:"Memby deployment and Emby outage or recovery notices. Maintenance mode itself still applies.",checked:b.systemAlerts,disabled:!b.enabled,onChange:E=>j(P=>P&&{...P,systemAlerts:E})})]}):null}),e.jsx(M,{title:"Settings",intro:"These live on the server and follow the person, so a change here reaches every television they use — usually within a few seconds, and on the next launch for a set that is switched off.",icon:"sliders",tone:"ok",actions:S.saved?e.jsxs(N,{tone:S.source==="admin"?"warn":"ok",children:["r",w(S.revision)," · ",S.source||"device"," · ",F(S.updatedAt)]}):e.jsx(N,{children:"defaults · never synced"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:i==="push",onClick:()=>void te("push",()=>q.put(`${l}/preferences`,{preferences:h??{}}),"Pushed to their televisions.",()=>x(null)),children:"Push to their televisions"}),e.jsx(T,{onClick:()=>{x(null),o()},children:"Discard changes"}),e.jsx(T,{onClick:()=>y({kind:"reset-preferences"}),children:"Restore defaults"}),e.jsx(Z,{className:"crumb",to:`/admin/accounts/${encodeURIComponent(s)}/settings`,children:"History and rollback →"})]}),children:ue.map(E=>e.jsxs("div",{className:"group",children:[e.jsx("p",{className:"group-label",children:E.name}),E.definitions.map(P=>e.jsx(bt,{definition:P,value:h==null?void 0:h[P.key],onChange:de=>x(me=>({...me??{},[P.key]:de}))},P.key))]},E.name))}),e.jsx(M,{title:"Colour schemes",intro:"Which palettes this person may choose between in Settings → Appearance. Tick everything to leave them unrestricted. Their current choice is an ordinary setting above; withdrawing it here puts them back on Midnight.",icon:"sparkle",tone:"note",actions:(I.themes??[]).length===0?e.jsx(N,{children:"all schemes"}):e.jsxs(N,{tone:"note",children:[w((I.themes??[]).length)," of ",w(le.length)]}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:i==="themes",onClick:()=>(u??[]).length===0?y({kind:"no-themes"}):void te("themes",()=>q.put(`${l}/themes`,{themes:u??[]}),"Colour schemes saved.",()=>g(null)),children:"Save colour schemes"}),e.jsx(T,{onClick:()=>g(le.map(E=>E.id)),children:"Allow all"}),e.jsxs("span",{className:"hint",children:["Seasonal themes are not listed. They apply to every television in the house for their dates and nobody can decline one — the only switch is ",e.jsx("em",{children:"Seasonal themes"})," on the features page."]})]}),children:e.jsx("div",{className:"checks columns",children:le.map(E=>{var P,de,me;return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:(u??[]).includes(E.id),onChange:Ie=>g(He=>Ie.target.checked?[...He??[],E.id]:(He??[]).filter(C=>C!==E.id))}),e.jsx("span",{className:"switch"}),e.jsx("span",{className:"swatch",style:{"--swatch-surface":js((P=E.palette)==null?void 0:P.surface),"--swatch-accent":js((de=E.palette)==null?void 0:de.accent),"--swatch-hairline":js((me=E.palette)==null?void 0:me.hairline)},children:e.jsx("i",{})}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:E.name}),e.jsx("p",{children:E.description})]})]},E.id)})})}),e.jsx(M,{title:"Remove Memby access",intro:"Signs every one of this person's Memby devices out. Their Emby account, viewing history and library permissions are untouched.",icon:"alert",tone:"bad",children:e.jsx(T,{variant:"danger",onClick:()=>y({kind:"remove-account"}),children:"Remove Memby access"})}),f?e.jsx(yt,{initial:f.name,busy:i==="rename",onCancel:()=>p(null),onConfirm:E=>void te("rename",()=>q.put(`${l}/devices/${encodeURIComponent(f.id)}`,{deviceName:E}),"Device renamed.",()=>p(null))}):null,R?e.jsx(wt,{initial:R.name,renaming:R.id!==null,busy:i==="viewer-name",onCancel:()=>k(null),onConfirm:E=>void te("viewer-name",()=>R.id?q.put(`${l}/viewers/${encodeURIComponent(R.id)}`,{name:E}):q.post(`${l}/viewers`,{name:E}),R.id?"Viewer renamed.":"Viewer added.",()=>{k(null),$.reload()})}):null,m?e.jsx(kt,{pending:m,busy:i,username:I.username,onCancel:()=>y(null),onConfirm:()=>{switch(m.kind){case"remove-device":return void te("remove-device",()=>q.del(`${l}/devices/${encodeURIComponent(m.deviceId)}`),"Device signed out.");case"remove-viewer":return void te("remove-viewer",()=>q.del(`${l}/viewers/${encodeURIComponent(m.viewer.id)}`),"Viewer removed.",()=>void $.reload());case"remove-account":return void te("remove-account",()=>q.del(`${l}/sessions`),"Memby access removed.",()=>n("/admin/accounts"));case"reset-recommendations":case"cancel-prompt":return void te("reset-rec",()=>q.del(`${l}/recommendations`),"Recommendation choices cleared.");case"reset-preferences":return void te("reset-prefs",()=>q.del(`${l}/preferences`),"Defaults restored.",()=>x(null));case"no-themes":return void te("themes",()=>q.put(`${l}/themes`,{themes:[]}),"Colour schemes saved.",()=>g(null))}}}):null]})}function ft({prompt:s}){const n=s.ratings??[],t=[["Genres",s.genres],["Studios",s.studios],["Actors",s.actors],["Actresses",s.actresses],["Directors",s.directors],["Types",s.contentTypes]],i=[...n.map(a=>e.jsxs(oe,{tone:"warn",children:[a.title," · ",w(a.rating)," ★"]},`r:${a.title}`)),...t.flatMap(([a,l])=>(l??[]).map(r=>e.jsxs(oe,{children:[a,": ",r]},`${a}:${r}`)))];return i.length===0?e.jsx(ee,{children:"No recommendation selections have been saved."}):e.jsx("div",{className:"chips",children:i})}function bt({definition:s,value:n,onChange:t}){if(s.kind==="toggle")return e.jsx(G,{label:s.name,hint:s.description,checked:!!n,onChange:t});if(s.kind==="choice"||s.kind==="number"){const a=s.unit??"",l=s.kind==="number"?(s.numbers??[]).map(r=>({value:String(r),label:r===0?"No limit":a?`${r} ${a}`:String(r)})):s.options??[];return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("select",{value:String(n??""),onChange:r=>t(s.kind==="number"?Number(r.target.value):r.target.value),children:l.map(r=>e.jsx("option",{value:r.value,children:r.label},r.value))})})}if(s.kind==="multi"){const a=Array.isArray(n)?n:[],l=[...a,...(s.options??[]).map(r=>r.value).filter(r=>!a.includes(r))];return e.jsxs("div",{className:"field",children:[e.jsx("span",{children:s.name}),e.jsx("small",{children:s.description}),e.jsx("div",{className:"checks",children:l.map(r=>{const d=(s.options??[]).find(c=>c.value===r);return d?e.jsx(G,{label:d.label,checked:a.includes(r),onChange:c=>t(c?[...a,r]:a.filter(o=>o!==r))},r):null})})]})}if(s.kind==="text")return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("input",{type:"text",value:String(n??""),maxLength:s.maxLength,placeholder:s.placeholder,onChange:a=>t(s.uppercase?a.target.value.toLocaleUpperCase("en-NZ"):a.target.value)})});const i=Array.isArray(n)?n:[];return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("textarea",{spellCheck:!1,placeholder:"One row id per line",value:i.join(` +`),onChange:a=>t(a.target.value.split(` +`).map(l=>l.trim()).filter(Boolean))})})}function yt({initial:s,busy:n,onConfirm:t,onCancel:i}){const[a,l]=v.useState(s||"Memby TV");return e.jsx("div",{className:"scrim",onPointerDown:r=>r.target===r.currentTarget&&i(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:"Name this device"}),e.jsx("p",{children:"The name a viewer sees in Settings → Devices, and what the console calls it."}),e.jsx(D,{label:"Device name",children:e.jsx("input",{type:"text",value:a,autoFocus:!0,maxLength:80,onChange:r=>l(r.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{variant:"quiet",onClick:i,children:"Cancel"}),e.jsx(T,{variant:"primary",busy:n,disabled:!a.trim(),onClick:()=>t(a.trim()),children:"Rename"})]})]})})}function wt({initial:s,renaming:n,busy:t,onConfirm:i,onCancel:a}){const[l,r]=v.useState(s);return e.jsx("div",{className:"scrim",onPointerDown:d=>d.target===d.currentTarget&&a(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:n?"Rename this viewer":"Add a viewer"}),e.jsx("p",{children:"The name shown on the television’s “Who’s watching?” screen. Everything they watch is kept by Memby and never reported to Emby."}),e.jsx(D,{label:"Name",children:e.jsx("input",{type:"text",value:l,autoFocus:!0,maxLength:40,onChange:d=>r(d.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{variant:"quiet",onClick:a,children:"Cancel"}),e.jsx(T,{variant:"primary",busy:t,disabled:!l.trim(),onClick:()=>i(l.trim()),children:n?"Rename":"Add viewer"})]})]})})}function kt({pending:s,busy:n,username:t,onConfirm:i,onCancel:a}){const r={"remove-device":{title:"Sign this device out of Memby?",body:"Its Emby account will not be changed. The set can sign in again at any time.",label:"Sign out",destructive:!0},"remove-account":{title:`Remove Memby access for ${t||"this user"}?`,body:"Every Memby device will be signed out. Their Emby account, viewing history and library permissions are untouched.",label:"Remove access",destructive:!0},"reset-recommendations":{title:"Clear this person's stored recommendation choices?",body:"Viewing history remains intact; only the explicit setup answers are removed.",label:"Clear",destructive:!0},"cancel-prompt":{title:"Cancel this person's queued recommendation prompt?",body:"They will not be invited to set up recommendations on their next launch.",label:"Cancel prompt",destructive:!1},"reset-preferences":{title:"Restore the Memby defaults for this person?",body:"Their televisions will pick the change up the next time they check in.",label:"Restore defaults",destructive:!0},"remove-viewer":{title:"Remove this viewer?",body:"What they were part-way through, what they had watched and their favourites are deleted, on every television in the house. The Emby account is untouched.",label:"Remove viewer",destructive:!0},"no-themes":{title:"Allow this person no colour schemes?",body:"They will be left on Midnight with nothing to choose between.",label:"Save anyway",destructive:!0}}[s.kind];return e.jsx(ke,{title:r.title,body:r.body,confirmLabel:r.label,destructive:r.destructive,busy:!!n,onConfirm:i,onCancel:a})}function Nt(s,n){if(s.kind==="toggle")return n?"On":"Off";if(s.kind==="choice"){const i=(s.options??[]).find(a=>a.value===n);return i?i.label:String(n??"")}if(s.kind==="number")return Number(n)===0&&s.unit?"No limit":s.unit?`${n} ${s.unit}`:String(n??"");const t=Array.isArray(n)?n:[];return t.length===0?"None":t.map(i=>{var a;return((a=(s.options??[]).find(l=>l.value===i))==null?void 0:a.label)??i}).join(", ")}function St(){const{userId:s=""}=Be(),{wrap:n}=ie(),{busy:t,run:i}=ne(),a=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:l,error:r,loading:d,reload:c}=J(`${a}/preferences/history`,{pollMs:3e4}),[o,h]=v.useState(new Set),[x,u]=v.useState(null),g=(l==null?void 0:l.username)||"this account",b=(l==null?void 0:l.devices)??[],j=(l==null?void 0:l.revisions)??[],m=(l==null?void 0:l.catalogue)??[],y=p=>h(R=>{const k=new Set(R);return k.has(p)?k.delete(p):k.add(p),k}),f=p=>i("restore",async()=>{await n(()=>q.post(`${a}/preferences/revisions/${p}/restore`),`Restored r${p}.`),u(null),await c()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Settings history",intro:`Every change to ${g}'s synced settings, and which of their televisions has taken it.`,crumbs:e.jsxs(Z,{to:`/admin/accounts/${encodeURIComponent(s)}`,children:["← ",g]})}),e.jsx(_,{message:r}),d?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Where each device has got to",intro:"A set takes a change by fetching it, which it does within a few seconds of being told — so anything still behind is switched off, mid-film, or cannot reach the gateway.",icon:"tv",tone:"info",actions:l!=null&&l.saved?e.jsxs(N,{tone:l.currentSource==="admin"?"warn":"ok",children:["now on r",w(l.currentRevision)," · ",l.currentSource||"device"]}):e.jsx(N,{children:"defaults · never synced"}),children:b.length===0?e.jsx(ee,{children:"No television has been signed in to this account."}):e.jsx("div",{className:"list",children:b.map(p=>{const R=p.never?void 0:p.behind?"bad":"ok";return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[e.jsx("span",{className:"dot-state","data-tone":R})," ",p.name||"Memby TV"," ",p.signedOut?e.jsx(N,{children:"signed out"}):null]}),e.jsxs("p",{children:[p.never?"Has not fetched these settings yet":`Holding r${w(p.revision)} · taken ${F(p.ackedAt)}`,p.clientVersion?` · Memby ${p.clientVersion}`:"",p.signedOut?"":` · last seen ${F(p.lastSeen)}`]})]}),e.jsx("div",{className:"list-actions",children:p.never?e.jsx(N,{children:"never taken one"}):p.behind?e.jsxs(N,{tone:"bad",children:[w(p.behind)," behind"]}):e.jsx(N,{tone:"ok",children:"up to date"})})]},p.deviceId||p.name)})})}),e.jsx(M,{title:"Change history",intro:"Restoring puts an earlier version back as a new change, so the televisions notice it and the version it replaced stays here to return to.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{className:"num",children:"Rev"}),e.jsx("th",{children:"Changed by"}),e.jsx("th",{children:"What changed"}),e.jsx("th",{className:"num",children:"Taken by"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:j.length===0?e.jsx(Y,{columns:6,children:"Nothing has been changed on this account yet."}):j.flatMap(p=>{const R=o.has(p.revision),k=p.acks??[],$=p.changes??[],I=[e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(p.createdAt)}),e.jsxs("td",{className:"num nowrap",children:["r",w(p.revision)," ",p.current?e.jsx(N,{tone:"ok",children:"current"}):null]}),e.jsxs("td",{className:"nowrap",children:[e.jsx(N,{tone:p.source==="admin"?"warn":"ok",children:p.author}),p.restoredFrom?e.jsxs("span",{className:"muted",children:[" restored r",w(p.restoredFrom)]}):null]}),e.jsx("td",{className:"muted",children:p.initial?e.jsx("span",{className:"muted",children:"First recorded settings"}):$.length===0?e.jsx("span",{className:"muted",children:"No visible change"}):e.jsx("div",{className:"chips",children:$.map((A,L)=>e.jsxs(oe,{children:[A.name,": ",A.before," → ",A.after]},`${A.name}:${L}`))})}),e.jsx("td",{className:"num",children:k.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{title:k.map(A=>A.deviceName||A.deviceId).join(", "),children:w(k.length)})}),e.jsx("td",{className:"num nowrap",children:e.jsxs("span",{className:"list-actions",children:[e.jsx(T,{size:"sm",onClick:()=>y(p.revision),children:R?"Hide":"Show"}),p.current?null:e.jsx(T,{size:"sm",onClick:()=>u(p.revision),children:"Restore"})]})})]},p.revision)];return R&&I.push(e.jsx("tr",{children:e.jsx("td",{colSpan:6,className:"muted",children:e.jsx("div",{className:"chips",children:m.map(A=>{var L;return e.jsxs(oe,{children:[A.name,":"," ",Nt(A,(L=p.preferences)==null?void 0:L[A.key])]},A.key)})})})},`${p.revision}:detail`)),I})})]})})})]}),x!==null?e.jsx(ke,{title:`Restore revision ${x}?`,body:"It goes out as a new change, so every one of their televisions will pick it up — and the current version stays in this history to return to.",confirmLabel:"Restore",busy:t==="restore",onConfirm:()=>void f(x),onCancel:()=>u(null)}):null]})}function Ct({client:s}){const n=s.versions??[];return n.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{className:"versions",children:n.map(t=>e.jsx(oe,{tone:t.version===s.version?"ok":void 0,children:t.version},t.version))})}function Mt(){const{status:s,error:n,loading:t}=je(),i=(s==null?void 0:s.clients)??[],a=i.filter(r=>(r.capabilities??[]).includes("server_features_v1")),l=new Set(i.map(r=>r.version).filter(Boolean));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands."}),e.jsx(_,{message:n}),t?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"devices known",value:w(i.length),icon:"tv",tone:"info"},{label:"active in the last quarter hour",value:w(i.filter(r=>Ye(r.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"reporting their capabilities",value:w(a.length),icon:"sliders",tone:"ok"},{label:"app builds in service",value:w(l.size),icon:"download",tone:"note"}]}),e.jsx(M,{title:"Devices",intro:"Every request carries what that build understands. A feature is only presented to a device that declares its contract, which is what lets an older set keep working while a new one gets the new behaviour. Status is whether the set is reporting that list at all; a build old enough to say nothing is served the fallback.",icon:"tv",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Device"}),e.jsx("th",{children:"Person"}),e.jsx("th",{children:"App"}),e.jsx("th",{children:"Builds seen"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Last seen"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(Y,{columns:6,children:"No devices have signed in yet."}):i.map(r=>{const d=(r.capabilities??[]).includes("server_features_v1"),c=Ns(r.lastSeen);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":c.tone,title:c.label}),e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||"Memby TV"})]})}),e.jsx("td",{className:"muted",children:r.username}),e.jsx("td",{className:"mono",children:r.version||"legacy"}),e.jsx("td",{children:e.jsx(Ct,{client:r})}),e.jsx("td",{children:e.jsx(N,{tone:d?"ok":"warn",children:d?"reported":"missing"})}),e.jsx("td",{className:"nowrap muted",children:F(r.lastSeen)})]},`${r.deviceId}:${r.username}`)})})]})})})]})]})}const Ws={user:"",q:"",ip:"",outcome:"",from:"",to:""},Tt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Rt(){var p,R,k,$,I,A;const[s,n]=v.useState("log"),[t,i]=v.useState(7),[a,l]=v.useState(Ws),[r,d]=v.useState(0),c=100,o=v.useMemo(()=>Ve({...a,days:a.from?void 0:t||void 0,limit:c,offset:r*c}),[a,t,r]),h=J(`/admin/api/logins${o}`,{enabled:s==="log"}),x=J(`/admin/api/logins/devices${o}`,{enabled:s==="devices"}),u=((p=h.data)==null?void 0:p.users)??((R=x.data)==null?void 0:R.users)??[],g=((k=h.data)==null?void 0:k.totals)??(($=x.data)==null?void 0:$.totals),b=((I=h.data)==null?void 0:I.retentionDays)??((A=x.data)==null?void 0:A.retentionDays)??90,j=s==="log"?h.loading:x.loading,m=s==="log"?h.error:x.error,y=L=>{l(X=>({...X,...L})),d(0)},f=Object.entries(a).some(([,L])=>L!=="")||!!a.from;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Sign-in history",intro:"Every connection attempt, kept as history rather than as the latest state of a device. A television that has since been removed still appears here, because it still connected.",actions:e.jsx(Ue,{value:s,options:[{value:"log",label:"Log"},{value:"devices",label:"By device"}],onChange:n})}),e.jsx(_,{message:m}),g?e.jsx(ae,{tiles:[{label:"Successful sign-ins",value:w(g.logins),icon:"key",tone:"ok"},{label:"Refused",value:w(g.failures),icon:"shield",tone:g.failures>0?"warn":void 0},{label:"Televisions",value:w(g.devices),icon:"tv",tone:"info"},{label:"People",value:w(g.users),icon:"people",tone:"note"},{label:"Addresses",value:w(g.addresses),icon:"globe",tone:"data"},{label:"History kept",value:`${b} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Ue,{value:a.from?-1:t,options:Tt.map(L=>({value:L.value,label:L.label})),onChange:L=>{i(L),y({from:"",to:""})}})}),e.jsx(D,{label:"Person",children:e.jsxs("select",{value:a.user,onChange:L=>y({user:L.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),u.map(L=>e.jsx("option",{value:L.id,children:L.username||L.id},L.id))]})}),e.jsx(D,{label:"Outcome",children:e.jsxs("select",{value:a.outcome,onChange:L=>y({outcome:L.target.value}),children:[e.jsx("option",{value:"",children:"Both"}),e.jsx("option",{value:"success",children:"Got in"}),e.jsx("option",{value:"failure",children:"Refused"})]})}),e.jsx(D,{label:"Address",children:e.jsx("input",{type:"text",value:a.ip,placeholder:"10.0.0.4",onChange:L=>y({ip:L.target.value})})}),e.jsx(D,{label:"From",children:e.jsx("input",{type:"date",value:a.from,onChange:L=>y({from:L.target.value})})}),e.jsx(D,{label:"To",children:e.jsx("input",{type:"date",value:a.to,onChange:L=>y({to:L.target.value})})}),e.jsx(D,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:a.q,placeholder:"Name, device or address",onChange:L=>y({q:L.target.value})})}),e.jsx("div",{className:"filter-actions",children:f?e.jsx(T,{variant:"quiet",size:"sm",onClick:()=>{l(Ws),d(0)},children:"Clear"}):null})]}),j?e.jsx(V,{}):s==="log"?e.jsx($t,{data:h.data,page:r,limit:c,onPage:d}):e.jsx(Et,{data:x.data})]})}function $t({data:s,page:n,limit:t,onPage:i}){if(!s)return null;const a=s.events.length,l=s.total===0?0:n*t+1;return e.jsxs(e.Fragment,{children:[e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Attempts per day",intro:"Grouped in the server's own timezone, so an evening sign-in stays on the day it happened.",icon:"chart",tone:"info",children:e.jsx(hs,{data:s.days,labelOf:r=>r.day,valueOf:r=>r.logins+r.failures,toneOf:r=>r.failures>r.logins?"bad":void 0,title:r=>`${r.day}: ${r.logins} in, ${r.failures} refused, ${r.devices} televisions`})}),e.jsx(M,{title:"Where from",icon:"globe",tone:"data",children:s.addresses.length===0?e.jsx(ee,{children:"No addresses in this window."}):e.jsx("div",{className:"list",children:s.addresses.slice(0,8).map(r=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:r.ipAddress}),e.jsxs("p",{children:[w(r.logins)," in",r.failures>0?` · ${w(r.failures)} refused`:""]})]}),r.failures>0&&r.logins===0?e.jsx(N,{tone:"bad",children:"only refused"}):null]},r.ipAddress))})})]}),e.jsx(M,{title:"Attempts",intro:"Uncollapsed and newest first: this is what to read when somebody says a television will not sign in.",icon:"key",tone:"ok",actions:e.jsx("span",{className:"filter-summary",children:s.total===0?"nothing matches":`${w(l)}–${w(l+a-1)} of ${w(s.total)}`}),footer:s.total>t?e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",disabled:n===0,onClick:()=>i(n-1),children:"Newer"}),e.jsx(T,{size:"sm",disabled:(n+1)*t>=s.total,onClick:()=>i(n+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Person"}),e.jsx("th",{children:"Television"}),e.jsx("th",{className:"nowrap",children:"Address"}),e.jsx("th",{children:"Build"}),e.jsx("th",{children:"Outcome"})]})}),e.jsx("tbody",{children:s.events.length===0?e.jsx(Y,{columns:6,children:"No sign-in attempts match these filters."}):s.events.map(r=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(r.occurredAt)}),e.jsx("td",{children:r.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{children:r.deviceId?e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||r.deviceId}):e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"mono nowrap",children:r.ipAddress||"—"}),e.jsx("td",{className:"mono",children:r.clientVersion||"—"}),e.jsx("td",{className:"nowrap",children:r.success?r.newDevice?e.jsx(N,{tone:"info",children:"first sign-in"}):e.jsx(N,{tone:"ok",children:"got in"}):e.jsx(N,{tone:"bad",children:r.failureReason||"refused"})})]},r.id))})]})})})]})}function Et({data:s}){return s?e.jsx(M,{title:"Televisions",intro:"Grouped from the history, not from the session list — a set whose session has expired still connected, and this is the record of it.",icon:"tv",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Television"}),e.jsx("th",{children:"Person"}),e.jsx("th",{className:"num",children:"Today"}),e.jsx("th",{className:"num",children:"Sign-ins"}),e.jsx("th",{className:"num",children:"Refused"}),e.jsx("th",{className:"num",children:"Addresses"}),e.jsx("th",{className:"nowrap",children:"Last address"}),e.jsx("th",{className:"nowrap",children:"Last sign-in"}),e.jsx("th",{children:"Build"})]})}),e.jsx("tbody",{children:s.devices.length===0?e.jsx(Y,{columns:9,children:"No television has connected in this window."}):s.devices.map(n=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(Z,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(n.deviceId)}`,children:n.deviceName||n.deviceId})}),e.jsx("td",{className:"muted",children:n.username||"—"}),e.jsx("td",{className:"num",children:n.loginsToday>0?w(n.loginsToday):"—"}),e.jsx("td",{className:"num",children:w(n.logins)}),e.jsx("td",{className:"num",children:n.failures>0?e.jsx("span",{className:"mono",children:w(n.failures)}):"—"}),e.jsx("td",{className:"num",children:w(n.distinctIps)}),e.jsx("td",{className:"mono nowrap",children:n.lastIp||"—"}),e.jsx("td",{className:"nowrap muted",children:n.lastLogin?F(n.lastLogin):"—"}),e.jsx("td",{className:"mono",children:n.clientVersion||"—"})]},n.deviceId))})]})})}):null}const At=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function It(){const{deviceId:s=""}=Be(),[n,t]=v.useState(7),i=v.useMemo(()=>`/admin/api/logins/devices/${encodeURIComponent(s)}${Ve({days:n||void 0,limit:200})}`,[s,n]),{data:a,error:l,loading:r}=J(i,{enabled:!!s}),d=a==null?void 0:a.summary,c=(d==null?void 0:d.deviceName)||s;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:c,intro:"One television's whole relationship with the gateway.",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(Z,{to:"/admin/clients",children:"Devices"}),e.jsx("span",{children:"/"}),e.jsx(Z,{to:"/admin/logins",children:"Sign-ins"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:c})]}),actions:e.jsx(Ue,{value:n,options:At.map(o=>({value:o.value,label:o.label})),onChange:t})}),e.jsx(_,{message:l}),r?e.jsx(V,{}):a?e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Sign-ins today",value:w((d==null?void 0:d.loginsToday)??0),icon:"clock",tone:"ok"},{label:"Sign-ins in total",value:w((d==null?void 0:d.logins)??0),icon:"key",tone:"info"},{label:"Refused",value:w((d==null?void 0:d.failures)??0),icon:"shield",tone:((d==null?void 0:d.failures)??0)>0?"warn":void 0},{label:"Addresses seen",value:w((d==null?void 0:d.distinctIps)??0),icon:"globe",tone:"data"},{label:"First seen",value:d!=null&&d.firstLogin?F(d.firstLogin):"—",small:!0,icon:"history"},{label:"Last seen",value:d!=null&&d.lastLogin?F(d.lastLogin):"—",small:!0,icon:"pulse",tone:"note"}]}),e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Connections per day",icon:"chart",tone:"info",children:e.jsx(hs,{data:a.days,labelOf:o=>o.day,valueOf:o=>o.logins+o.failures,toneOf:o=>o.failures>o.logins?"bad":void 0,title:o=>`${o.day}: ${o.logins} in${o.failures?`, ${o.failures} refused`:""}`})}),e.jsxs("div",{className:"stack",children:[e.jsx(M,{title:"Identity",icon:"tv",tone:"info",children:e.jsxs("div",{className:"list",children:[e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Person"}),e.jsx("p",{children:(d==null?void 0:d.username)||"unknown"})]})}),e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Device id"}),e.jsx("p",{className:"mono",children:a.deviceId})]})}),e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Running"}),e.jsx("p",{className:"mono",children:(d==null?void 0:d.clientVersion)||"unknown"})]})})]})}),e.jsx(M,{title:"Builds",intro:"Kept per television rather than per session, so it survives a sign-out.",icon:"upload",tone:"note",children:a.versions.length===0?e.jsx(ee,{children:"No build history for this television."}):e.jsx("div",{className:"list",children:a.versions.map(o=>e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:o.version}),e.jsxs("p",{children:[F(o.firstSeen)," → ",F(o.lastSeen)]})]})},o.version))})})]})]}),e.jsx(M,{title:"Addresses",icon:"globe",tone:"data",children:a.addresses.length===0?e.jsx(ee,{children:"No addresses recorded in this window."}):e.jsx("div",{className:"chips",children:a.addresses.map(o=>e.jsxs(oe,{tone:o.failures>0?"warn":"data",children:[o.ipAddress," · ",w(o.logins),o.failures>0?` (+${w(o.failures)} refused)`:""]},o.ipAddress))})}),e.jsx(M,{title:"Every attempt",icon:"key",tone:"ok",actions:e.jsxs("span",{className:"filter-summary",children:[w(a.total)," in this window"]}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Person"}),e.jsx("th",{className:"nowrap",children:"Address"}),e.jsx("th",{children:"Build"}),e.jsx("th",{children:"Method"}),e.jsx("th",{children:"Outcome"})]})}),e.jsx("tbody",{children:a.events.length===0?e.jsx(Y,{columns:6,children:"This television has not connected in the selected window."}):a.events.map(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(o.occurredAt)}),e.jsx("td",{children:o.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{className:"mono nowrap",children:o.ipAddress||"—"}),e.jsx("td",{className:"mono",children:o.clientVersion||"—"}),e.jsx("td",{className:"muted",children:o.method}),e.jsx("td",{className:"nowrap",children:o.success?o.newDevice?e.jsx(N,{tone:"info",children:"first sign-in"}):e.jsx(N,{tone:"ok",children:"got in"}):e.jsx(N,{tone:"bad",children:o.failureReason||"refused"})})]},o.id))})]})})})]}):null]})}function Lt(){const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(!1),o=(s==null?void 0:s.library.byType)??{},h=!!(s!=null&&s.syncRunning),x=u=>r(u,async()=>{await a(()=>q.post("/admin/api/sync",{kind:u}),u==="full"?"Full re-import started.":"Import started."),c(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Library",intro:"Import and inspect the catalogue Memby ranks."}),e.jsx(_,{message:n}),t||!s?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"items",value:w(s.library.total),icon:"library",tone:"data"},...Object.keys(o).sort().map(u=>({label:u,value:w(o[u]),icon:"list"})),{label:"last import",value:F(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsx(M,{title:"Import the catalogue",intro:"Emby's catalogue is copied here so search and the recommendation candidate pool can be answered from one indexed table. Watched, favourite and resume state is deliberately not stored — that is per person and still comes from Emby live.",icon:"library",tone:"data",footer:e.jsx("span",{className:"hint",children:h?"Import running…":`An incremental import runs automatically every ${s.syncEvery}.`}),children:e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",icon:"sync",disabled:h,busy:l==="incremental",onClick:()=>void x("incremental"),children:"Sync new items"}),e.jsx(T,{icon:"database",disabled:h,onClick:()=>c(!0),children:"Full re-import"})]})})]}),d?e.jsx(ke,{title:"Re-import the entire library?",body:"A full pass mark-and-sweeps the catalogue and can take several minutes on a large library. Televisions keep reading the current table throughout.",confirmLabel:"Re-import",busy:l==="full",onConfirm:()=>void x("full"),onCancel:()=>c(!1)}):null]})}function Dt(){var x;const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),d=(s==null?void 0:s.requestUsers)??[],c=((x=s==null?void 0:s.requestPolicy)==null?void 0:x.allowedUserIds)??[],o=v.useMemo(()=>new Map(((s==null?void 0:s.requestUsage)??[]).map(u=>[u.userId,u])),[s==null?void 0:s.requestUsage]),h=u=>r(`access-${u}`,async()=>{const g=c.includes(u)?c.filter(b=>b!==u):[...c,u];await a(()=>q.post("/admin/api/request-policy",{allowedUserIds:g}),g.includes(u)?"Request access granted.":"Request access removed."),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Media requests",intro:"Who can ask for something the library does not have."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Where a request goes",intro:"Movies follow Radarr’s policy. Series follow the dedicated Sonarr request policy: monitored as normal, with backlog searching only when an operator enables it under Integrations.",icon:"inbox",tone:"info",actions:e.jsxs(e.Fragment,{children:[e.jsxs(N,{tone:s!=null&&s.radarrReady?"ok":"bad",children:["Movies ",s!=null&&s.radarrReady?"ready":"not configured"]}),e.jsxs(N,{tone:s!=null&&s.sonarrReady?"ok":"bad",children:["Series ",s!=null&&s.sonarrReady?"ready":"not configured"]})]}),children:!(s!=null&&s.radarrReady)&&!(s!=null&&s.sonarrReady)?e.jsx(ee,{children:"Neither Radarr nor Sonarr is configured, so a request would have nowhere to go. The button stays hidden on every television until one of them is."}):null}),e.jsx(M,{title:"Request access and activity",intro:"One button grants or removes access. A recorded request has already been sent to Radarr or Sonarr; Memby does not duplicate their download state.",icon:"people",tone:"note",children:d.length===0?e.jsx(ee,{children:"No one has signed in yet."}):e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"User"}),e.jsx("th",{children:"Last seen"}),e.jsx("th",{className:"num",children:"Sent to services"}),e.jsx("th",{children:"Last request"}),e.jsx("th",{children:"Access"})]})}),e.jsx("tbody",{children:d.map(u=>{const g=o.get(u.id),b=c.includes(u.id);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("b",{children:u.username})}),e.jsx("td",{className:"muted nowrap",children:F(u.lastSeen)}),e.jsx("td",{className:"num",children:(g==null?void 0:g.requests)??0}),e.jsx("td",{className:"muted nowrap",children:g!=null&&g.lastRequest?F(g.lastRequest):"—"}),e.jsx("td",{children:e.jsx(T,{size:"sm",variant:b?"quiet":"primary",busy:l===`access-${u.id}`,onClick:()=>void h(u.id),children:b?"Remove access":"Give access"})})]},u.id)})})]})})})]})]})}function qt(){const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(!1),o=s==null?void 0:s.forYou,h=!!(s!=null&&s.forYouRunning),x=(u,g,b)=>r(g,async()=>{await a(()=>q.post("/admin/api/for-you",{action:u}),b),c(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"For You",intro:"The prepared pools personalised rows are drawn from."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Tracearr sessions",value:w((o==null?void 0:o.tracearrSessions)??0),icon:"play",tone:"info"},{label:"user profiles",value:w((o==null?void 0:o.profiles)??0),icon:"people",tone:"note"},{label:"ranked candidates",value:w((o==null?void 0:o.candidates)??0),icon:"sparkle",tone:"note"},{label:"last full import",value:F(o==null?void 0:o.lastFullImport),small:!0,icon:"clock"}]}),e.jsx(M,{title:"Pool maintenance",intro:"Prepared pools refresh in the background; these are the manual versions of the same work. A rebuild is safe at any time — televisions read the last finished pool until a new one lands.",icon:"sparkle",tone:"note",footer:e.jsx("span",{className:"hint",children:h?"For You maintenance running…":"Prepared pools normally refresh in the background."}),children:e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",icon:"download",disabled:h,busy:l==="import",onClick:()=>void x("incremental-import","import","Import started."),children:"Import recent sessions"}),e.jsx(T,{icon:"database",disabled:h,onClick:()=>c(!0),children:"Full Tracearr backfill"}),e.jsx(T,{icon:"sync",disabled:h,busy:l==="rebuild",onClick:()=>void x("rebuild-all","rebuild","Rebuild started."),children:"Rebuild all pools"})]})}),e.jsx(M,{title:"Reading a person's scores",intro:"The inspector re-runs the shared weighted scorer over one person's prepared pool, after Emby permission and parental-control filtering, and shows every component and evidence reason behind the order.",icon:"search",tone:"info",actions:e.jsx(Z,{to:"/admin/inspector",children:"Open the inspector"}),children:e.jsx(e.Fragment,{})})]}),d?e.jsx(ke,{title:"Backfill all Tracearr history?",body:"Every session is re-read and every active user's pool is rebuilt. It is safe at any time — televisions keep reading the last finished pool — but on a long history it takes a while.",confirmLabel:"Backfill",busy:l==="full",onConfirm:()=>void x("full-import","full","Backfill started."),onCancel:()=>c(!1)}):null]})}const Ft=[["Genre","genres"],["Studio","studios"],["Actor","actors"],["Director","directors"],["Franchise","franchises"],["Runtime","runtimeRanges"],["Age rating","ageRatings"],["Community rating","communityRatings"],["Release period","releasePeriods"],["Content type","contentTypes"]];function Ot(s){return s?Ft.flatMap(([n,t])=>Object.entries(s[t]??{}).map(([i,a])=>({dimension:n,name:i,weight:a.weight??0,evidence:a.evidence??0}))).sort((n,t)=>Math.abs(t.weight)-Math.abs(n.weight)):[]}const Bs=s=>`${s>=0?"+":""}${s.toFixed(3)}`;function Pt(){const{status:s}=je(),{wrap:n}=ie(),{busy:t,run:i}=ne(),[a,l]=v.useState(""),[r,d]=v.useState("default"),[c,o]=v.useState("0"),[h,x]=v.useState(""),[u,g]=v.useState(null),[b,j]=v.useState("Choose a person to inspect their recommendations."),[m,y]=v.useState(""),f=(s==null?void 0:s.requestUsers)??[],p=()=>i("run",async()=>{if(!a){y("Choose a person to pressure-test.");return}y(""),j("Running the permission check and the scorer…");const A=new URLSearchParams({userId:a,context:r,minutes:c||"0",limit:"100"});h&&A.set("at",new Date(h).toISOString());const L=await n(()=>q.get(`/admin/api/recommendations?${A.toString()}`));L?(g(L),j(`Scored at ${new Date().toLocaleTimeString()}.`)):j("Pressure test failed.")}),R=Ot((u==null?void 0:u.profile)??null).slice(0,24),k=(u==null?void 0:u.actions)??[],$=(u==null?void 0:u.items)??[],I=(u==null?void 0:u.profileMeta)??{};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Score inspector",intro:"Re-run the ranker for one person and read every component."}),e.jsx(_,{message:m}),e.jsx(M,{title:"Run a pressure test",intro:"Nothing is changed by running this. It scores the person's prepared pool as the launcher would, in the context you choose.",icon:"search",tone:"info",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:t==="run",onClick:()=>void p(),children:"Run pressure test"}),e.jsx("span",{className:"hint",children:b})]}),children:e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Person",children:e.jsxs("select",{value:a,onChange:A=>l(A.target.value),children:[e.jsx("option",{value:"",children:"Choose a person…"}),f.map(A=>e.jsx("option",{value:A.id,children:A.username},A.id))]})}),e.jsx(D,{label:"Context",children:e.jsxs("select",{value:r,onChange:A=>d(A.target.value),children:[e.jsx("option",{value:"default",children:"Default"}),e.jsx("option",{value:"bedtime",children:"One episode before bed"}),e.jsx("option",{value:"hidden",children:"Hidden library"}),e.jsx("option",{value:"new-releases",children:"Recent new releases"})]})}),e.jsx(D,{label:"Available minutes",children:e.jsx("input",{type:"number",min:0,max:360,value:c,onChange:A=>o(A.target.value)})}),e.jsx(D,{label:"Evaluate at",children:e.jsx("input",{type:"datetime-local",value:h,onChange:A=>x(A.target.value)})})]})}),u?e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"prepared pool",value:w(u.poolCandidates),icon:"database",tone:"data"},{label:"permission eligible",value:w(u.permissionEligible),icon:"shield",tone:"ok"},{label:"ranked result",value:w($.length),icon:"sparkle",tone:"note"},{label:"source events",value:w(I.sourceEvents??0),icon:"pulse",tone:"info"},{label:"algorithm",value:I.algorithmVersion||"—",small:!0,icon:"chip"},{label:"pool built",value:F(I.poolBuiltAt),small:!0,icon:"clock"}]}),e.jsxs(M,{title:"Profile evidence",intro:"The strongest learned affinities, and every explicit action this person has taken.",icon:"sparkle",tone:"note",children:[R.length===0?e.jsx(ee,{children:"No repeated affinity evidence yet; cold-start priors apply."}):e.jsx("div",{className:"chips",children:R.map(A=>e.jsxs(oe,{tone:A.weight<0?"bad":void 0,children:[A.dimension,": ",A.name," ",Bs(A.weight)," · n=",w(A.evidence)]},`${A.dimension}:${A.name}`))}),k.length===0?e.jsx(ee,{children:"No explicit recommendation actions."}):e.jsx("div",{className:"chips",children:k.map((A,L)=>e.jsxs(oe,{tone:"ok",children:[A.action,": ",A.title||A.itemId]},`${A.action}:${L}`))})]}),$.length===0?e.jsx(M,{children:e.jsx(ee,{children:"No candidates survived this context, the explicit exclusions and the permission filter."})}):e.jsx(xe,{children:$.map((A,L)=>{const X=A.explanation??{},le=Object.entries(X.components??{}).sort((H,re)=>Math.abs(re[1])-Math.abs(H[1])),ue=A.exposure??{},te=[A.type,A.year,A.runtimeMinutes?`${A.runtimeMinutes} min`:null,...A.genres??[]].filter(Boolean).join(" · ");return e.jsxs(M,{title:`#${L+1} · ${A.title}`,intro:te,actions:e.jsx(oe,{tone:"ok",children:Number(X.total??0).toFixed(3)}),children:[e.jsxs("p",{className:"hint",children:[A.preparedReason||"No legacy prepared explanation",A.compatibilityLabel?` · ${A.compatibilityLabel}`:""]}),e.jsxs("div",{className:"chips",children:[(X.reasonCodes??[]).map(H=>e.jsx(oe,{tone:"ok",children:H},H)),le.map(([H,re])=>e.jsxs(oe,{tone:re<0?"bad":void 0,children:[H,"=",Bs(re)]},H))]}),e.jsxs("details",{children:[e.jsx("summary",{className:"muted",children:"Pool, row and exposure detail"}),e.jsxs("p",{className:"hint",children:["Base rank ",w(A.baseRank??0)," · base ",Number(A.baseScore??0).toFixed(3)," · affinity ",Number(A.affinityScore??0).toFixed(3)," · compatibility"," ",Number(A.compatibilityScore??0).toFixed(3)," · impressions"," ",w(ue.impressions??0)," · focuses ",w(ue.focuses??0)," · selects"," ",w(ue.selects??0)]}),e.jsx("div",{className:"chips",children:(A.eligibleRows??[]).map(H=>e.jsx(oe,{tone:"ok",children:H},H))}),A.preparedEvidenceTitle?e.jsxs("p",{className:"hint",children:["Prepared evidence: ",A.preparedEvidenceTitle]}):null]})]},`${L}:${A.title}`)})})]}):null]})}const _t=4,Ae=[{id:"home",label:"Home",type:"films and television shows"},{id:"movies",label:"Movies",type:"films"},{id:"tv_shows",label:"TV Shows",type:"television shows"}],ze=()=>({pinnedItems:[],primeSubtitle:""}),fs=[{value:1,short:"Mon",label:"Monday"},{value:2,short:"Tue",label:"Tuesday"},{value:3,short:"Wed",label:"Wednesday"},{value:4,short:"Thu",label:"Thursday"},{value:5,short:"Fri",label:"Friday"},{value:6,short:"Sat",label:"Saturday"},{value:0,short:"Sun",label:"Sunday"}];function Vs(s){const n=s.getTimezoneOffset()*6e4;return new Date(s.getTime()-n).toISOString().slice(0,16)}function Hs(s){return!!(s&&Number.isFinite(new Date(s).getTime())&&new Date(s).getFullYear()>=2e3)}function Ut(s){return s.frequency??"once"}function Wt(s){if(s.frequency==="daily")return`Every day · ${s.startTime}–${s.endTime}`;if(s.frequency==="weekly"){const n=fs.filter(i=>(s.weekdays??[]).includes(i.value));return`${(n.length===7?"Every day":n.map(i=>i.short).join(", "))||"No days selected"} · ${s.startTime}–${s.endTime}`}return`${s.startAt?new Date(s.startAt).toLocaleString():"Start missing"} → ${s.endAt?new Date(s.endAt).toLocaleString():"End missing"}`}function zs(s,n){const t=new Date;t.setMinutes(Math.ceil(t.getMinutes()/30)*30,0,0);const i=new Date(t.getTime()+2*60*60*1e3),a=n==="home"||n==="movies"&&s.type==="Movie"||n==="tv_shows"&&s.type==="Series";return{id:crypto.randomUUID(),itemId:s.id,startAt:t.toISOString(),endAt:i.toISOString(),priority:0,enabled:!0,placements:[a?n:"home"]}}function Bt(){var te,H,re;const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a,show:l}=ie(),{busy:r,run:d}=ne(),[c,o]=v.useState("home"),[h,x]=v.useState({home:ze(),movies:ze(),tv_shows:ze()}),[u,g]=v.useState(!1),[b,j]=v.useState(""),[m,y]=v.useState(null),[f,p]=v.useState([]),[R,k]=v.useState(null),$=s==null?void 0:s.heroPolicy;v.useEffect(()=>{var S,O,z;u||!$||(x({home:((S=$.placements)==null?void 0:S.home)??{pinnedItems:$.pinnedItems??[],primeSubtitle:$.primeSubtitle??""},movies:((O=$.placements)==null?void 0:O.movies)??ze(),tv_shows:((z=$.placements)==null?void 0:z.tv_shows)??ze()}),p($.schedules??[]))},[$,u]);const I=()=>d("search",async()=>{const S=b.trim();if(!S)return;const O=await a(()=>q.get(`/admin/api/hero/search?q=${encodeURIComponent(S)}`));O&&y(O.items??[])}),A=h[c],L=A.pinnedItems??[],X=S=>{x(O=>({...O,[c]:{...O[c],...S}})),g(!0)},le=S=>{if(!L.some(O=>O.id===S.id)){if(L.length>=_t){l("Remove a pinned title before adding another.","bad");return}X({pinnedItems:[...L,S]})}},ue=()=>d("save",async()=>{await a(()=>q.post("/admin/api/hero-policy",{placements:Object.fromEntries(Object.entries(h).map(([S,O])=>[S,{pinnedItemIds:(O.pinnedItems??[]).map(z=>z.id),primeSubtitle:O.primeSubtitle.trim()}])),schedules:f}),"Hero saved."),g(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Featured content",intro:"Manage an independent, backend-resolved hero for Home, Movies and TV Shows."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(xe,{children:Ae.map(S=>{var $e;const O=h[S.id],z=f.filter(ge=>ge.enabled&&(ge.placements??["home"]).includes(S.id)).sort((ge,E)=>E.priority-ge.priority)[0],ce=(O.pinnedItems??[]).length?"Manual":z?"Schedule ready":"Automatic",we=($e=$==null?void 0:$.items)==null?void 0:$e.find(ge=>ge.id===(z==null?void 0:z.itemId)),Re=(O.pinnedItems??[]).map(ge=>ge.name).join(", ")||(we==null?void 0:we.name)||(z==null?void 0:z.itemId)||"Resolved for each viewer";return e.jsx(M,{title:S.label,intro:`${ce} · ${Re}`,tone:S.id===c?"info":void 0,children:e.jsxs(T,{size:"sm",variant:"quiet",onClick:()=>o(S.id),children:["Manage ",S.label]})},S.id)})}),e.jsx("div",{className:"tabs",role:"tablist","aria-label":"Hero placement",children:Ae.map(S=>e.jsx(T,{variant:c===S.id?"primary":"quiet",onClick:()=>o(S.id),children:S.label},S.id))}),e.jsxs(M,{title:`${(te=Ae.find(S=>S.id===c))==null?void 0:te.label} hero`,intro:`Pinned ${(H=Ae.find(S=>S.id===c))==null?void 0:H.type} lead this section only. Empty places use this placement’s automatic selection.`,icon:"star",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:r==="save",onClick:()=>void ue(),children:"Save hero"}),e.jsx(T,{onClick:()=>{X({pinnedItems:[]})},children:"Clear pins"}),u?e.jsx("span",{className:"hint",children:"Unsaved changes."}):null]}),children:[L.length===0?e.jsx(ee,{children:"No titles are pinned. The hero is entirely release-aware and automatic."}):e.jsx("div",{className:"hero-pins",children:L.map((S,O)=>e.jsxs("div",{className:"hero-pin",children:[e.jsx("span",{className:"hero-pin-order",children:O+1}),e.jsxs("span",{children:[e.jsx("b",{children:S.name}),e.jsxs("small",{children:[S.type,S.year?` · ${S.year}`:""]})]}),e.jsx(T,{size:"sm",icon:"clock",onClick:()=>k(zs(S,c)),children:"Schedule"}),e.jsx(T,{variant:"quiet",size:"sm",icon:"close",title:`Remove ${S.name}`,onClick:()=>X({pinnedItems:L.filter(z=>z.id!==S.id)})})]},S.id))}),e.jsx(D,{label:"Prime-card subtitle",hint:"Optional wording under the large first card. Leave blank to use Memby's natural release or rating reason.",children:e.jsx("input",{type:"text",maxLength:160,value:A.primeSubtitle,placeholder:"Leave blank for the automatic reason",onChange:S=>{X({primeSubtitle:S.target.value})}})})]}),e.jsx(M,{title:"Hero schedule",intro:"The gateway applies these rules in server time. Manual pins win first; otherwise the highest-priority active schedule wins, followed by Memby’s automatic hero.",icon:"clock",tone:"info",actions:e.jsx(N,{tone:"info",children:($==null?void 0:$.timeZone)||"server local time"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:r==="save",onClick:()=>void ue(),children:"Save schedule"}),e.jsx("span",{className:"hint",children:"Daily and weekly rules repeat until you switch them off."})]}),children:f.length===0?e.jsx(ee,{children:"No scheduled heroes yet. Use Schedule beside a pinned or searched title."}):e.jsx("div",{className:"hero-schedule-list",children:[...f].sort((S,O)=>Number(O.enabled)-Number(S.enabled)||O.priority-S.priority).map(S=>{const O=[...($==null?void 0:$.items)??[],...L,...m??[]].find(z=>z.id===S.itemId);return e.jsxs("article",{className:"hero-schedule","data-enabled":S.enabled||void 0,children:[e.jsxs("div",{className:"hero-schedule-time",children:[e.jsx("b",{children:S.frequency==="weekly"?"Weekly":S.frequency==="daily"?"Daily":"Once"}),e.jsx("span",{children:S.frequency?S.startTime:S.startAt?new Date(S.startAt).toLocaleDateString():"—"})]}),e.jsxs("div",{className:"hero-schedule-main",children:[e.jsxs("div",{className:"hero-schedule-title",children:[e.jsx("h3",{children:(O==null?void 0:O.name)??S.itemId}),e.jsx(N,{tone:S.enabled?"ok":void 0,children:S.enabled?"enabled":"paused"})]}),e.jsx("p",{children:Wt(S)}),e.jsxs("div",{className:"chips",children:[(S.placements??["home"]).map(z=>{var ce;return e.jsx("span",{className:"chip",children:(ce=Ae.find(we=>we.id===z))==null?void 0:ce.label},z)}),S.priority!==0?e.jsxs("span",{className:"chip",children:["Priority ",S.priority]}):null]})]}),e.jsxs("div",{className:"hero-schedule-actions",children:[e.jsx(T,{size:"sm",onClick:()=>k({...S}),children:"Edit"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>{p(z=>z.map(ce=>ce.id===S.id?{...ce,enabled:!ce.enabled}:ce)),g(!0)},children:S.enabled?"Pause":"Enable"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>{p(z=>z.filter(ce=>ce.id!==S.id)),g(!0)},children:"Remove"})]})]},S.id)})})}),e.jsxs(M,{title:"Find a title",intro:`Search the imported Emby catalogue. Add a result to the selected ${(re=Ae.find(S=>S.id===c))==null?void 0:re.label} placement; switch tabs to show it in more than one section.`,icon:"search",tone:"info",children:[e.jsxs("div",{className:"field-row",children:[e.jsx(D,{label:"Title",grow:!0,children:e.jsx("input",{type:"search",value:b,placeholder:"Search films and television shows",onChange:S=>j(S.target.value),onKeyDown:S=>{S.key==="Enter"&&I()}})}),e.jsx(T,{busy:r==="search",icon:"search",onClick:()=>void I(),children:"Search"})]}),m===null?null:m.length===0?e.jsx(ee,{children:"No playable films or series matched that search."}):e.jsx(xe,{children:m.map(S=>e.jsx(M,{title:S.name,intro:`${S.type||"Title"} · ${S.year||"Year unknown"}`,children:e.jsxs("div",{className:"row",children:[e.jsx(T,{size:"sm",icon:"plus",disabled:L.some(O=>O.id===S.id)||c==="movies"&&S.type!=="Movie"||c==="tv_shows"&&S.type!=="Series",onClick:()=>le(S),children:L.some(O=>O.id===S.id)?"Pinned":"Add to hero"}),e.jsx(T,{size:"sm",icon:"clock",onClick:()=>k(zs(S,c)),children:"Schedule"})]})},S.id))})]})]}),R?e.jsx(Vt,{schedule:R,item:[...($==null?void 0:$.items)??[],...L,...m??[]].find(S=>S.id===R.itemId),timeZone:($==null?void 0:$.timeZone)||"server local time",isNew:!f.some(S=>S.id===R.id),onCancel:()=>k(null),onSave:S=>{p(O=>O.some(z=>z.id===S.id)?O.map(z=>z.id===S.id?S:z):[...O,S]),k(null),g(!0)}}):null]})}function Vt({schedule:s,item:n,timeZone:t,isNew:i,onSave:a,onCancel:l}){const[r,d]=v.useState({...s,weekdays:[...s.weekdays??[]]}),c=Ut(r),o=j=>{const m=new Date,y=new Date(m.getTime()+2*60*60*1e3);d(f=>{var p;return j==="once"?{...f,frequency:void 0,startAt:Hs(f.startAt)?f.startAt:m.toISOString(),endAt:Hs(f.endAt)?f.endAt:y.toISOString()}:{...f,frequency:j,startTime:f.startTime||"18:00",endTime:f.endTime||"22:00",weekdays:j==="weekly"?(p=f.weekdays)!=null&&p.length?f.weekdays:[1,2,3,4,5]:[]}})},h=r.placements??["home"],x=j=>j==="home"||j==="movies"&&(n==null?void 0:n.type)==="Movie"||j==="tv_shows"&&(n==null?void 0:n.type)==="Series",u=!!(r.startAt&&r.endAt&&new Date(r.endAt)>new Date(r.startAt)),g=!!(r.startTime&&r.endTime&&r.startTime!==r.endTime&&(c!=="weekly"||(r.weekdays??[]).length>0)),b=c==="once"?u:g;return e.jsx("div",{className:"scrim",onPointerDown:j=>j.target===j.currentTarget&&l(),children:e.jsxs("div",{className:"dialog hero-schedule-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"hero-schedule-title",children:[e.jsxs("div",{className:"hero-schedule-dialog-head",children:[e.jsx("span",{className:"hero-schedule-kicker",children:"Hero schedule"}),e.jsx("h2",{id:"hero-schedule-title",children:(n==null?void 0:n.name)??r.itemId}),e.jsxs("p",{children:["Choose exactly when this title can lead the selected sections. Times use ",t,"."]})]}),e.jsx("div",{className:"schedule-frequency",role:"group","aria-label":"Schedule frequency",children:["once","daily","weekly"].map(j=>e.jsxs("button",{type:"button","aria-pressed":c===j,onClick:()=>o(j),children:[e.jsx("b",{children:j==="once"?"One time":j==="daily"?"Every day":"Weekly"}),e.jsx("span",{children:j==="once"?"A date range":j==="daily"?"Same time daily":"Choose days"})]},j))}),c==="once"?e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts",children:e.jsx("input",{type:"datetime-local",value:r.startAt?Vs(new Date(r.startAt)):"",onChange:j=>d(m=>({...m,startAt:j.target.value?new Date(j.target.value).toISOString():void 0}))})}),e.jsx(D,{label:"Ends",children:e.jsx("input",{type:"datetime-local",value:r.endAt?Vs(new Date(r.endAt)):"",onChange:j=>d(m=>({...m,endAt:j.target.value?new Date(j.target.value).toISOString():void 0}))})})]}):e.jsxs(e.Fragment,{children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts each time",children:e.jsx("input",{type:"time",value:r.startTime??"",onChange:j=>d(m=>({...m,startTime:j.target.value}))})}),e.jsx(D,{label:"Ends each time",hint:"An earlier end time continues into the following day.",children:e.jsx("input",{type:"time",value:r.endTime??"",onChange:j=>d(m=>({...m,endTime:j.target.value}))})})]}),c==="weekly"?e.jsxs("div",{className:"schedule-days",children:[e.jsxs("div",{className:"schedule-days-head",children:[e.jsx("b",{children:"Days"}),e.jsxs("div",{children:[e.jsx("button",{type:"button",onClick:()=>d(j=>({...j,weekdays:[1,2,3,4,5]})),children:"Weekdays"}),e.jsx("button",{type:"button",onClick:()=>d(j=>({...j,weekdays:[6,0]})),children:"Weekend"}),e.jsx("button",{type:"button",onClick:()=>d(j=>({...j,weekdays:fs.map(m=>m.value)})),children:"Every day"})]})]}),e.jsx("div",{className:"schedule-day-grid",children:fs.map(j=>{const m=(r.weekdays??[]).includes(j.value);return e.jsx("button",{type:"button","aria-pressed":m,title:j.label,onClick:()=>d(y=>({...y,weekdays:m?(y.weekdays??[]).filter(f=>f!==j.value):[...y.weekdays??[],j.value]})),children:j.short},j.value)})})]}):null]}),e.jsxs("div",{className:"schedule-options",children:[e.jsxs("div",{children:[e.jsx("span",{className:"schedule-option-label",children:"Show in"}),e.jsx("div",{className:"schedule-placement-grid",children:Ae.map(j=>e.jsx(G,{label:j.label,checked:h.includes(j.id),disabled:!x(j.id),onChange:m=>d(y=>{const f=y.placements??["home"],p=m?[...f,j.id]:f.filter(R=>R!==j.id);return{...y,placements:p.length?[...new Set(p)]:f}})},j.id))})]}),e.jsx(D,{label:"Priority",hint:"Higher rules win when schedules overlap.",children:e.jsx("input",{type:"number",min:-1e3,max:1e3,step:10,value:r.priority,onChange:j=>d(m=>({...m,priority:Number(j.target.value)}))})})]}),e.jsx(G,{label:"Schedule enabled",hint:"Pause it without losing its days and times.",checked:r.enabled,onChange:j=>d(m=>({...m,enabled:j}))}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx(T,{variant:"primary",disabled:!b,onClick:()=>a(r),children:i?"Add rule":"Save rule"})]})]})})}const Ks=["title","ratings","facts","summary"];function Ht(){var b;const s=J("/admin/api/metadata-hero"),{busy:n,run:t}=ne(),{wrap:i}=ie(),[a,l]=v.useState(null),[r,d]=v.useState(null);v.useEffect(()=>{a===null&&s.data&&l(s.data.settings.contentOrder),r===null&&s.data&&d(s.data.settings.timeRemainingColour??"green")},[a,s.data,r]);const c=a??Ks,o=r??"green",h=((b=s.data)==null?void 0:b.options)??[],x=[...c,...h.map(j=>j.value).filter(j=>!c.includes(j))],u=(j,m)=>{const y=c.indexOf(j),f=y+m;if(y<0||f<0||f>=c.length)return;const p=[...c],R=p[y];p[y]=p[f],p[f]=R,l(p)},g=()=>t("save",async()=>{const j=await i(()=>q.post("/admin/api/metadata-hero",{contentOrder:c,timeRemainingColour:o}),"Metadata hero saved for every television.");j&&(s.set(j),l(j.settings.contentOrder),d(j.settings.timeRemainingColour))});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Metadata hero",intro:"Choose which content appears beside focused browse cards, and the order every television uses."}),e.jsx(_,{message:s.error}),s.loading?e.jsx(V,{rows:2}):e.jsxs("div",{className:"metadata-hero-layout",children:[e.jsxs(M,{title:"Content order",intro:"Enabled blocks are drawn from top to bottom. Changes apply to the whole household.",icon:"sliders",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:n==="save",onClick:()=>void g(),children:"Save metadata hero"}),e.jsx(T,{onClick:()=>{l(Ks),d("green")},children:"Restore defaults"})]}),children:[e.jsx("div",{className:"metadata-hero-order",children:x.map(j=>{const m=h.find(p=>p.value===j);if(!m)return null;const y=c.indexOf(j),f=y>=0;return e.jsxs("div",{className:"metadata-hero-order-row","data-enabled":f||void 0,children:[e.jsx("span",{className:"metadata-hero-order-number",children:f?y+1:"—"}),e.jsx(G,{label:m.label,hint:m.description,checked:f,disabled:f&&c.length===1,onChange:p=>l(p?[...c,j]:c.filter(R=>R!==j))}),e.jsxs("div",{className:"metadata-hero-order-actions",children:[e.jsx(T,{size:"sm",variant:"quiet",disabled:!f||y===0,title:`Move ${m.label} up`,onClick:()=>u(j,-1),children:"↑"}),e.jsx(T,{size:"sm",variant:"quiet",disabled:!f||y===c.length-1,title:`Move ${m.label} down`,onClick:()=>u(j,1),children:"↓"})]})]},j)})}),e.jsxs("div",{className:"metadata-hero-colour",children:[e.jsx("b",{children:"Time-remaining text colour"}),e.jsx("p",{children:"Choose the caption colour used beside the resume progress bar."}),e.jsx(Ue,{value:o,options:[{value:"green",label:"Emby green"},{value:"white",label:"White"}],onChange:d})]})]}),e.jsx(M,{title:"Television preview",intro:"A representative focused film at launcher scale and in the current order.",icon:"tv",children:e.jsxs("div",{className:"metadata-hero-preview","aria-label":"Metadata hero preview",children:[e.jsx("div",{className:"metadata-hero-preview-copy",children:c.map(j=>j==="title"?e.jsx("div",{className:"metadata-preview-title",children:"THE SOUTHERN LIGHT"},j):j==="ratings"?e.jsxs("div",{className:"metadata-preview-ratings",children:[e.jsx("b",{children:"IMDb 8.2"}),e.jsx("b",{children:"TMDb 81%"})]},j):j==="facts"?e.jsxs("div",{className:"metadata-preview-facts",children:[e.jsx("span",{children:"2026   •   1h 54m   •   M   •   Drama, Mystery"}),e.jsx("b",{children:"4K"}),e.jsx("b",{children:"5.1"})]},j):j==="recommendation_reason"?e.jsx("div",{className:"metadata-preview-reason",children:"Because you watched Harbour Lights"},j):j==="time_remaining"?e.jsxs("div",{className:"metadata-preview-progress","data-colour":o,children:[e.jsx("span",{children:e.jsx("i",{})}),e.jsx("b",{children:"47 minutes remaining"})]},j):j==="summary"?e.jsx("p",{className:"metadata-preview-summary",children:"A quiet coastal town follows an unexpected signal across the winter sky."},j):null)}),e.jsx("div",{className:"metadata-hero-preview-art","aria-hidden":"true"})]})})]})]})}function zt(){const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(null),o=s==null?void 0:s.features,h=(o==null?void 0:o.features)??[],x=(o==null?void 0:o.configuration)??[],u=(s==null?void 0:s.clients)??[],g=(o==null?void 0:o.revision)??0,b=(p,R,k,$)=>r(R,async()=>{await a(()=>q.post("/admin/api/features",{action:p,expectedRevision:g,overrides:$??{}}),k),c(null),await i()}),j=u.filter(p=>(p.capabilities??[]).includes("server_features_v1")).length,m=!!(o!=null&&o.safeMode),y=(p,R)=>({...Object.fromEntries(h.filter(k=>k.source==="override").map(k=>[k.key,k.enabled])),[p]:R}),f=(p,R)=>({...Object.fromEntries(x.map(k=>[k.key,k.value])),[p]:R});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Client configuration",intro:"One server-owned control plane for what Memby renders: flags, values, page composition and contextual discovery."}),e.jsx(_,{message:n}),t||!o?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Thin client control plane",intro:"The gateway decides which sections, heroes and discovery rows are delivered. The TV remains a fast renderer of known components and safely ignores anything newer.",icon:"tv",children:e.jsxs("div",{className:"chips",children:[e.jsx("a",{className:"chip",href:"/admin/hero",children:"Hero sources and pinned overrides"}),e.jsx("a",{className:"chip",href:"/admin/recommendations",children:"For You and recommendation pools"}),e.jsx("a",{className:"chip",href:"/admin/engagement",children:"Contextual row engagement signals"}),e.jsx("a",{className:"chip",href:"/admin/clients",children:"Client versions and capabilities"})]})}),e.jsx(M,{title:"Control plane",intro:"Every optional feature has a safe default, an explicit override and a remote recovery path. Safe mode turns all of them off at once; sign-in, browsing and playback are never optional.",icon:"sliders",tone:"ok",actions:e.jsx(T,{variant:m?void 0:"danger",busy:l==="safe",onClick:()=>m?void b("leave-safe-mode","safe","Safe mode ended."):c({action:"safe-mode",title:"Enable safe mode?",body:"Every optional feature is disabled immediately on every television. Core sign-in, browsing and playback remain available.",label:"Enable safe mode"}),children:m?"Leave safe mode":"Enable safe mode"}),children:e.jsx(Cs,{tiles:[{label:"features active",value:`${h.filter(p=>p.enabled).length} / ${h.length}`},{label:"explicit overrides",value:w(h.filter(p=>p.source==="override").length)},{label:"televisions reporting the control plane",value:`${j} / ${u.length}`},{label:"published revision",value:`r${w(g)}`}]})}),e.jsx(M,{title:"Central configuration",intro:"Typed values use the same revisioned control plane as feature flags. Global values are safe defaults; user, device and experimental scopes are available to the client contract as features grow.",icon:"sliders",children:e.jsx("div",{className:"stack",children:x.map(p=>{const R=p.value;return e.jsxs("div",{className:"row",children:[e.jsxs("div",{className:"grow",children:[e.jsx("strong",{children:p.name}),e.jsxs("div",{className:"hint",children:[p.description," · ",p.scopes.join(", ")]}),e.jsx(oe,{children:p.key})]}),p.type==="boolean"?e.jsx(G,{label:R?"On":"Off",checked:!!R,disabled:l===p.key,onChange:k=>void r(p.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:f(p.key,k),overrides:Object.fromEntries(h.filter($=>$.source==="override").map($=>[$.key,$.enabled]))}),"Configuration saved."),await i()})}):p.type==="enum"?e.jsx("select",{value:String(R),disabled:l===p.key,onChange:k=>void r(p.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:f(p.key,k.target.value),overrides:Object.fromEntries(h.filter($=>$.source==="override").map($=>[$.key,$.enabled]))}),"Configuration saved."),await i()}),children:(p.options??[]).map(k=>e.jsx("option",{value:k,children:k},k))}):p.type==="json"?e.jsx("textarea",{rows:4,value:JSON.stringify(R,null,2),disabled:l===p.key,"aria-label":p.name,onChange:k=>{try{const $=JSON.parse(k.target.value);r(p.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:f(p.key,$),overrides:Object.fromEntries(h.filter(I=>I.source==="override").map(I=>[I.key,I.enabled]))}),"Configuration saved."),await i()})}catch{}}}):e.jsx("input",{type:"number",value:Number(R),min:p.min,max:p.max,disabled:l===p.key,onChange:k=>void r(p.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:f(p.key,Number(k.target.value)),overrides:Object.fromEntries(h.filter($=>$.source==="override").map($=>[$.key,$.enabled]))}),"Configuration saved."),await i()})})]},p.key)})})}),e.jsx(xe,{cols:"2",children:h.length===0?e.jsx(M,{title:"Nothing registered",icon:"sliders",children:e.jsx(ee,{children:"No server features are registered."})}):h.map(p=>e.jsxs(M,{title:p.name,intro:p.description,actions:e.jsx(N,{tone:p.enabled?"ok":void 0,children:p.enabled?"active":"off"}),footer:e.jsxs("span",{className:"hint",children:["↳ ",p.recovery]}),children:[e.jsx(G,{label:p.enabled?"On":"Off",hint:"Changing this applies the feature policy to every compatible television.",checked:p.enabled,disabled:l===p.key,onChange:R=>c({action:"feature",key:p.key,enabled:R,title:`${R?"Turn on":"Turn off"} ${p.name}?`,body:`${R?"Enable":"Disable"} this feature for every compatible television. ${p.recovery}`,label:R?"Turn on":"Turn off"})}),e.jsxs("div",{className:"chips",children:[e.jsx(oe,{children:p.key}),e.jsxs(oe,{children:["protocol ",w(p.minimumProtocol),"+"]}),e.jsx(oe,{tone:p.compatible?"ok":"warn",children:p.compatible?"server compatible":"compatibility blocked"}),e.jsx(oe,{tone:"note",children:p.area})]})]},p.key))}),e.jsx(M,{children:e.jsxs("div",{className:"row",children:[e.jsx(T,{disabled:!o.canRollback,onClick:()=>c({action:"rollback",title:"Roll back one revision?",body:"The previous published feature revision is restored on every television.",label:"Roll back"}),children:"Roll back one revision"}),e.jsx(T,{onClick:()=>c({action:"reset",title:"Clear every override?",body:"All features return to their safe software defaults.",label:"Clear overrides"}),children:"Clear all overrides"}),e.jsx("span",{className:"spacer"}),m?e.jsx(N,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(N,{tone:"ok",children:["live · revision r",w(g)]})]})})]}),d?e.jsx(ke,{title:d.title,body:d.body,confirmLabel:d.label,destructive:d.action!=="rollback",busy:l===d.action,onConfirm:()=>void b(d.action==="feature"?"save":d.action,d.action==="feature"?d.key??"feature":d.action,`${d.label} done.`,d.action==="feature"&&d.key?y(d.key,!!d.enabled):void 0),onCancel:()=>c(null)}):null]})}function Kt(){const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a,show:l}=ie(),{busy:r,run:d}=ne(),[c,o]=v.useState(!0),[h,x]=v.useState("6.5"),[u,g]=v.useState(!1);v.useEffect(()=>{var j,m;u||!s||(o(((j=s.playbackPolicy)==null?void 0:j.prerollEnabled)!==!1),x(String((((m=s.playbackPolicy)==null?void 0:m.prerollDurationMs)??6500)/1e3)))},[s,u]);const b=()=>d("save",async()=>{const j=Number(h);if(!Number.isFinite(j)||j<1||j>30){l("The preroll duration must be between 1 and 30 seconds.","bad");return}await a(()=>q.post("/admin/api/playback-policy",{prerollEnabled:c,prerollDurationMs:Math.round(j*1e3)}),"Playback policy saved."),g(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Playback",intro:"Presentation policy sent with every playback launch."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:1}):e.jsxs(M,{title:"Upcoming-show preroll",intro:"Sent with every playback launch. A change applies to the next title opened on every gateway-connected television; no app release is required.",icon:"play",tone:"info",actions:c?e.jsxs(N,{tone:"ok",children:["on · ",h,"s"]}):e.jsx(N,{children:"off"}),footer:e.jsx(T,{variant:"primary",busy:r==="save",onClick:()=>void b(),children:"Save playback policy"}),children:[e.jsx(G,{label:"Show the preroll before a title starts",checked:c,onChange:j=>{o(j),g(!0)}}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Duration",hint:"Between 1 and 30 seconds. The stream is already playing behind it.",children:e.jsx("input",{type:"number",min:1,max:30,step:.5,value:h,onChange:j=>{x(j.target.value),g(!0)}})})})]})]})}function Gt(){const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(null),[o,h]=v.useState(null),[x,u]=v.useState(!1),g=s==null?void 0:s.subtitles,b=g==null?void 0:g.stored;v.useEffect(()=>{d||!g||c({bazarr:g.bazarrEnabled,openSubtitles:g.openSubtitlesEnabled,key:"",clearKey:!1,username:g.openSubtitlesUsername??"",password:"",clearLogin:!1})},[g,d]);const j=p=>c(R=>R&&{...R,...p}),m=()=>r("save",async()=>{d&&(await a(()=>q.post("/admin/api/subtitle-settings",{bazarrEnabled:d.bazarr,openSubtitlesEnabled:d.openSubtitles,openSubtitlesApiKey:d.key.trim(),clearOpenSubtitlesApiKey:d.clearKey,openSubtitlesUsername:d.username.trim(),openSubtitlesPassword:d.password,clearOpenSubtitlesLogin:d.clearLogin}),"Subtitle settings saved."),c(null),await i())}),y=()=>r("test",async()=>{h(null);const p=await a(()=>q.post("/admin/api/subtitle-test"));h((p==null?void 0:p.results)??[])}),f=()=>r("clear",async()=>{await a(()=>q.post("/admin/api/subtitle-settings",{action:"clear-stored"}),"Stored subtitles deleted."),u(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from."}),e.jsx(_,{message:n}),t||!g||!d?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"offered on televisions",value:g.available?"yes":"no",small:!0,icon:"captions",tone:g.available?"ok":void 0},{label:"providers on",value:w((g.bazarrEnabled&&g.bazarrConfigured?1:0)+(g.openSubtitlesEnabled?1:0)),icon:"list",tone:"note"},{label:"subtitles held",value:w((b==null?void 0:b.count)??0),icon:"database",tone:"data"},{label:"last fetched",value:F(b==null?void 0:b.latest),small:!0,icon:"clock"}]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"Bazarr",intro:"Bazarr writes the subtitle file beside the media file, so Emby finds it and the track behaves like one that was always there. Its address is deployment configuration; this switch only decides whether viewers may use it.",icon:"wrench",tone:"data",actions:g.bazarrConfigured?d.bazarr?e.jsx(N,{tone:"ok",children:"on"}):e.jsx(N,{children:"off"}):e.jsx(N,{children:"not configured"}),footer:e.jsx("span",{className:"hint",children:g.bazarrConfigured?`Configured at ${g.bazarrUrl}`:"Set MEMBY_BAZARR_URL and MEMBY_BAZARR_API_KEY to use Bazarr."}),children:e.jsx(G,{label:"Offer Bazarr in the player",hint:"Off leaves every subtitle it has already written in place.",checked:d.bazarr,disabled:!g.bazarrConfigured,onChange:p=>j({bazarr:p})})}),e.jsxs(M,{title:"OpenSubtitles",intro:"OpenSubtitles hands back a file rather than writing one, so Memby keeps what it fetches and serves it to the television itself. Titles are matched on their IMDb or TMDb id, which is exact — there is no guessing at a name.",icon:"captions",tone:"note",actions:g.openSubtitlesEnabled?e.jsx(N,{tone:g.openSubtitlesAccount?"ok":"warn",children:g.openSubtitlesAccount?"on · signed in":"on · anonymous"}):e.jsx(N,{children:g.openSubtitlesKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(G,{label:"Offer OpenSubtitles in the player",hint:"Needs an API key. It cannot be switched on without one.",checked:d.openSubtitles,onChange:p=>j({openSubtitles:p})}),e.jsx(D,{label:"API key",hint:"From your consumer at opensubtitles.com. Leave blank to keep the saved key.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:d.key,placeholder:g.openSubtitlesKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:p=>j({key:p.target.value})})}),e.jsx(G,{label:"Remove the saved key",checked:d.clearKey,onChange:p=>j({clearKey:p})}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Account username",hint:"Optional, and the difference between a working feature and one that stops after a few files: without an account, downloads come out of the small anonymous allowance.",children:e.jsx("input",{type:"text",autoComplete:"off",value:d.username,placeholder:"Not signed in",onChange:p=>j({username:p.target.value})})}),e.jsx(D,{label:"Account password",hint:"Leave blank to keep the saved one.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:d.password,onChange:p=>j({password:p.target.value})})})]}),e.jsx(G,{label:"Sign out and forget the account",checked:d.clearLogin,onChange:p=>j({clearLogin:p})})]})]}),e.jsxs(M,{children:[e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>void m(),children:"Save subtitle settings"}),e.jsx(T,{busy:l==="test",icon:"pulse",onClick:()=>void y(),children:"Test the providers"}),e.jsx("span",{className:"hint",children:g.featureEnabled?"A change applies to the next title opened; no app release is required.":"Downloading subtitles is switched off on the Features page, so nothing here is offered."})]}),o===null?null:o.length===0?e.jsx(ee,{children:"No provider is switched on, so there was nothing to ask."}):e.jsx("div",{className:"list",children:o.map(p=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:p.provider}),e.jsx("p",{children:p.message})]}),e.jsx("div",{className:"list-actions",children:e.jsx(N,{tone:p.ok?"ok":"bad",children:p.ok?"reachable":"not reachable"})})]},p.provider))})]}),e.jsx(M,{title:"Subtitles Memby is holding",intro:"Only files fetched from a provider that cannot write beside the media file are kept here; they are served to televisions as ordinary tracks on every later playback. Emptying this is safe — each one can be fetched again, at the cost of the download allowance that fetched it.",icon:"database",tone:"data",actions:b!=null&&b.count?e.jsxs(N,{tone:"data",children:[w(b.count)," files · ",he(b.bytes)]}):e.jsx(N,{children:"nothing held"}),footer:e.jsx(T,{variant:"danger",disabled:!(b!=null&&b.count),onClick:()=>u(!0),children:"Delete every stored subtitle"}),children:e.jsx(e.Fragment,{})})]}),x?e.jsx(ke,{title:"Delete every stored subtitle?",body:"Each one can be fetched again, at the cost of the download allowance that fetched it. Subtitles Bazarr wrote beside the media are untouched — those belong to Emby.",confirmLabel:"Delete",destructive:!0,busy:l==="clear",onConfirm:()=>void f(),onCancel:()=>u(!1)}):null]})}function Zt(){const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(null),[o,h]=v.useState(!1),x=s==null?void 0:s.updatePolicy;v.useEffect(()=>{d||!x||c({version:x.latestVersion??"",url:x.downloadUrl??"",notes:x.notes??"",retireBelow:x.retireBelowVersion??"",required:!!x.minimumVersion&&x.minimumVersion===x.latestVersion,destructive:!!x.retireBelowVersion&&x.retireBelowVersion===x.latestVersion})},[x,d]);const u=m=>r(m?"save":"off",async()=>{d&&(await a(()=>q.post("/admin/api/update-policy",{enabled:m,latestVersion:d.version.trim(),downloadUrl:d.url.trim(),notes:d.notes.trim(),required:d.required,destructive:d.destructive,retireBelowVersion:d.retireBelow.trim()}),m?"Update policy saved.":"Update prompts turned off."),h(!1),c(null),await i())}),g=!!(x!=null&&x.minimumVersion)&&(x==null?void 0:x.minimumVersion)===(x==null?void 0:x.latestVersion),b=!!(x!=null&&x.retireBelowVersion)&&(x==null?void 0:x.retireBelowVersion)===(x==null?void 0:x.latestVersion),j=m=>c(y=>y&&{...y,...m});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"App updates",intro:"Publish an optional or a required client update."}),e.jsx(_,{message:n}),t||!d?e.jsx(V,{rows:1}):e.jsxs(M,{title:"Update policy",intro:"Televisions check on every launch. An optional update is a prompt the viewer can dismiss; a required one covers the home screen until they update, so it needs a download URL that actually works.",icon:"download",tone:"info",actions:x!=null&&x.enabled?e.jsxs(N,{tone:g?"warn":"ok",children:[b?"sign-out · ":g?"required · ":"optional · ",x.latestVersion]}):e.jsx(N,{children:"off"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>d.required?h(!0):void u(!0),children:"Save policy"}),e.jsx(T,{busy:l==="off",onClick:()=>void u(!1),children:"Turn prompts off"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Latest version",children:e.jsx("input",{type:"text",value:d.version,placeholder:"0.2.63",onChange:m=>j({version:m.target.value})})}),e.jsx(D,{label:"APK URL",children:e.jsx("input",{type:"text",value:d.url,placeholder:"https://nas/memby/memby-0.2.63.apk",onChange:m=>j({url:m.target.value})})})]}),e.jsx(D,{label:"What's new",hint:"Shown on the television above the update button.",children:e.jsx("input",{type:"text",value:d.notes,placeholder:"One line the viewer reads",onChange:m=>j({notes:m.target.value})})}),e.jsx(D,{label:"Sign out builds below",hint:"The destructive compatibility floor. Leave blank to keep every supported viewer signed in.",children:e.jsx("input",{type:"text",value:d.retireBelow,placeholder:"0.2.44",onChange:m=>j({retireBelow:m.target.value})})}),e.jsx(G,{label:"Require this update",hint:"Blocks the home screen on every television below this version.",checked:d.required,onChange:m=>j({required:m})}),e.jsx(G,{label:"Set the destructive floor to this update",hint:"Deletes sessions on every older television when it next uses Memby, then shows the required update screen.",checked:d.destructive,onChange:m=>j(m?{destructive:!0,required:!0,retireBelow:d.version.trim()}:{destructive:!1,retireBelow:d.retireBelow.trim()===d.version.trim()?"":d.retireBelow})})]}),o&&d?e.jsx(ke,{title:d.destructive?"Sign every older television out?":"Require this update?",body:d.destructive?"This deletes sessions on every older television and forces viewers to sign in again after updating.":"Required updates block the home screen on every television below this version until they update.",confirmLabel:"Publish",destructive:d.destructive,busy:l==="save",onConfirm:()=>void u(!0),onCancel:()=>h(!1)}):null]})}const Yt=[{value:60,label:"Every minute"},{value:300,label:"Every 5 minutes"},{value:600,label:"Every 10 minutes"},{value:900,label:"Every 15 minutes"},{value:1800,label:"Every 30 minutes"},{value:3600,label:"Hourly"},{value:10800,label:"Every 3 hours"},{value:21600,label:"Every 6 hours"},{value:43200,label:"Every 12 hours"},{value:86400,label:"Daily"},{value:604800,label:"Weekly"}];function Jt(s){const n=[...Yt];for(const t of[s.defaultIntervalSeconds,s.intervalSeconds])t>0&&!n.some(i=>i.value===t)&&n.push({value:t,label:Ne(t).replace(/^every /,"Every ")});return n.sort((t,i)=>t.value-i.value)}const Ge=s=>s.defaultIntervalSeconds>0&&s.intervalSeconds!==s.defaultIntervalSeconds;function us(s){return s==="failed"?"bad":s==="running"?"info":s==="skipped"?"warn":"ok"}function ls(s){if(s.running)return{state:"running",label:"Running",tone:"info",title:"Running right now"};if(!s.enabled)return{state:"disabled",label:"Disabled",tone:"warn",title:"Switched off — it will not run on its schedule"};const n=s.lastRun;return n?n.status==="failed"?{state:"failed",label:"Failed",tone:"bad",title:n.error||"The last run failed"}:n.status==="skipped"?{state:"skipped",label:"Skipped",tone:"warn",title:n.detail||"The last run declined to do anything"}:{state:"successful",label:"Successful",tone:"ok",title:"The last run finished cleanly"}:{state:"never",label:"Never run",tone:void 0,title:"This task has not run since the gateway started recording"}}function Qt(s){if(s.state==="running"||s.state==="successful")return"ok";if(s.state==="failed")return"bad";if(s.state==="disabled"||s.state==="skipped")return"warn"}const Gs={failed:0,running:1,skipped:2,disabled:3,never:4,successful:5};function Xt(s,n){const t=Gs[ls(s).state]-Gs[ls(n).state];if(t!==0)return t;if(!!s.group!=!!n.group)return s.group?-1:1;const i=s.group.localeCompare(n.group);return i!==0?i:s.name.localeCompare(n.name)}const ea=2e4,sa=3e3;function na(){const{wrap:s}=ie(),{busy:n,run:t}=ne(),[i,a]=v.useState(!1),{data:l,error:r,loading:d,reload:c}=J("/admin/api/tasks?limit=60",{pollMs:i?sa:ea}),o=(l==null?void 0:l.tasks)??[],h=o.some(m=>m.running);h!==i&&a(h);const x=m=>t(m.id,async()=>{await s(()=>q.post(`/admin/api/tasks/${encodeURIComponent(m.id)}/run`),`${m.name} started.`),await c()}),u=o.filter(m=>{var y;return((y=m.lastRun)==null?void 0:y.status)==="failed"}).length,g=o.filter(Ge).length,b=o.filter(m=>!m.enabled).length,j=[...o].sort(Xt);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Scheduled tasks",intro:"The gateway's background work: what it does, when it last ran, how long it took and whether it worked. Every one of these can be started by hand."}),e.jsx(_,{message:r}),d?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Tasks",value:w(o.length),icon:"clock",tone:"info"},{label:"Running now",value:w(o.filter(m=>m.running).length),icon:"pulse",tone:h?"ok":void 0},{label:"Last run failed",value:w(u),icon:"alert",tone:u>0?"bad":void 0},{label:"Switched off",value:w(b),icon:"power",tone:b>0?"warn":void 0},{label:"Retimed",value:w(g),icon:"clock",tone:g>0?"note":void 0}]}),u>0?e.jsx(pe,{tone:"bad",children:"A failed task publishes an administrative event, so the failure is in the activity feed and wherever your integrations send it — you did not have to be looking at this page."}):null,e.jsx(M,{title:"Tasks",intro:"Anything wrong sorts to the top. Open a task to change its schedule, switch it off, or read its own run history.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Service"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Status"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:j.length===0?e.jsx(Y,{columns:8,children:"This gateway has no scheduled tasks registered."}):j.map(m=>{const y=ls(m),f=m.lastRun,p=`/admin/tasks/${encodeURIComponent(m.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(Z,{className:"table-row-link",to:p,children:m.name}),e.jsx("span",{className:"table-sub",children:m.description})]}),e.jsx("td",{className:"muted nowrap",children:m.group||"Other"}),e.jsxs("td",{className:"nowrap",children:[Ne(m.intervalSeconds),Ge(m)?e.jsxs("span",{className:"table-sub",children:["default ",Ne(m.defaultIntervalSeconds)]}):null]}),e.jsxs("td",{className:"nowrap muted",title:f?F(f.startedAt):void 0,children:[f?fe(f.startedAt):"never",f!=null&&f.error?e.jsx("span",{className:"table-sub",children:f.error}):null]}),e.jsx("td",{className:"num muted",children:f?ye(f.durationMs):"—"}),e.jsx("td",{className:"nowrap muted",children:m.enabled?m.running?"now":ds(m.nextRun):"—"}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":Qt(y)}),e.jsx(N,{tone:y.tone,children:e.jsx("span",{title:y.title,children:y.label})})]})}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(T,{size:"sm",icon:"play",busy:n===m.id,disabled:m.running,title:`Run ${m.name} now`,onClick:()=>void x(m)}),e.jsx(Z,{className:"table-row-link",to:p,children:"Details"})]})})]},m.id)})})]})})}),e.jsx(M,{title:"Recent runs",intro:"Every task together and in order, which is what shows two jobs interfering with each other.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Detail"})]})}),e.jsx("tbody",{children:((l==null?void 0:l.runs.length)??0)===0?e.jsx(Y,{columns:6,children:"No task has run yet."}):l==null?void 0:l.runs.map(m=>{var y;return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(m.startedAt),children:fe(m.startedAt)}),e.jsx("td",{children:e.jsx(Z,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(m.taskId)}`,children:((y=o.find(f=>f.id===m.taskId))==null?void 0:y.name)??m.taskId})}),e.jsx("td",{className:"muted",children:m.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(m.status),children:m.status})}),e.jsx("td",{className:"num muted",children:ye(m.durationMs)}),e.jsx("td",{className:"muted",children:m.error||m.detail||"—"})]},m.id)})})]})})})]})]})}const ta=2e4,aa=3e3,ia=100;function ra(s){const n=s.filter(l=>l.status!=="running"),t=n.filter(l=>l.status==="failed").length,i=n.filter(l=>l.durationMs>0),a=i.length?i.reduce((l,r)=>l+r.durationMs,0)/i.length:0;return{runs:n.length,failed:t,averageMs:a,timed:i.length}}function la(){var y;const{taskId:s=""}=Be(),{wrap:n}=ie(),{busy:t,run:i}=ne(),[a,l]=v.useState(!1),{data:r,error:d,loading:c,reload:o}=J(`/admin/api/tasks?task=${encodeURIComponent(s)}&limit=${ia}`,{pollMs:a?aa:ta,enabled:!!s}),h=r==null?void 0:r.tasks.find(f=>f.id===s),x=!!(h!=null&&h.running);x!==a&&l(x);const u=(r==null?void 0:r.runs)??[],g=h?ls(h):void 0,b=ra(u),j=(f,p,R)=>i(f,async()=>{await n(()=>q.put(`/admin/api/tasks/${encodeURIComponent(s)}`,R),p),await o()}),m=f=>i("run",async()=>{await n(()=>q.post(`/admin/api/tasks/${encodeURIComponent(s)}/run`),`${f.name} started.`),await o()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:(h==null?void 0:h.name)||s,intro:h==null?void 0:h.description,icon:"clock",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(Z,{to:"/admin/tasks",children:"Scheduled tasks"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:(h==null?void 0:h.name)||s})]}),actions:h?e.jsx(T,{variant:"primary",icon:"play",busy:t==="run",disabled:h.running,onClick:()=>void m(h),children:h.running?"Running":"Run now"}):void 0}),e.jsx(_,{message:d}),c?e.jsx(V,{}):h?e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Status",value:(g==null?void 0:g.label)??"—",small:!0,icon:"pulse",tone:g==null?void 0:g.tone},{label:"Service",value:h.group||"Other",small:!0,icon:"chip",tone:"info"},{label:"Runs every",value:Ne(h.intervalSeconds),small:!0,icon:"clock",tone:Ge(h)?"note":void 0},{label:"Next run",value:h.enabled?h.running?"now":ds(h.nextRun):"not scheduled",small:!0,icon:"history",tone:h.enabled?void 0:"warn"},{label:`Failed of the last ${w(b.runs)}`,value:w(b.failed),icon:"alert",tone:b.failed>0?"bad":void 0},{label:`Average of ${w(b.timed)} timed runs`,value:b.timed?ye(Math.round(b.averageMs)):"—",small:!0,icon:"chart",tone:"data"}]}),(y=h.lastRun)!=null&&y.error?e.jsxs(pe,{tone:"bad",children:["The last run failed: ",h.lastRun.error]}):null,e.jsxs(M,{title:"Schedule",intro:"How often the gateway runs this on its own, and whether it runs it at all.",icon:"sliders",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx(D,{label:"How often it runs",children:e.jsx("select",{"aria-label":`How often ${h.name} runs`,value:h.intervalSeconds,disabled:t==="interval"||h.running,onChange:f=>void j("interval",`${h.name} now runs ${Ne(Number(f.target.value))}.`,{intervalSeconds:Number(f.target.value)}),children:Jt(h).map(f=>e.jsxs("option",{value:f.value,children:[f.label,f.value===h.defaultIntervalSeconds?" (default)":""]},f.value))})}),Ge(h)?e.jsxs(T,{icon:"refresh",busy:t==="interval",disabled:h.running,onClick:()=>void j("interval",`${h.name} back to its default cadence.`,{intervalSeconds:0}),children:["Back to ",Ne(h.defaultIntervalSeconds)]}):null]}),e.jsx(G,{label:"Run this on its schedule",hint:"Switched off, the gateway leaves it alone. You can still start it by hand.",checked:h.enabled,disabled:t==="enabled",onChange:f=>void j("enabled",f?`${h.name} switched on.`:`${h.name} switched off.`,{enabled:f})}),Ge(h)?e.jsxs(pe,{tone:"note",children:["This task is retimed: its code asks for ",Ne(h.defaultIntervalSeconds)," and it is set to ",Ne(h.intervalSeconds),"."]}):null]}),e.jsx(M,{title:"Run history",intro:"This task alone, newest first — which is what separates a job that fails occasionally from one that has stopped working.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Detail"})]})}),e.jsx("tbody",{children:u.length===0?e.jsx(Y,{columns:5,children:"This task has not run yet."}):u.map(f=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(f.startedAt),children:fe(f.startedAt)}),e.jsx("td",{className:"muted",children:f.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(f.status),children:f.status})}),e.jsx("td",{className:"num muted",children:ye(f.durationMs)}),e.jsx("td",{className:"muted",children:f.error||f.detail||"—"})]},f.id))})]})})})]}):e.jsx(M,{title:"No such task",icon:"alert",tone:"bad",children:e.jsxs(ee,{children:["This gateway has no task called ",e.jsx("code",{children:s}),". It may have been renamed or removed."," ",e.jsx(Z,{to:"/admin/tasks",children:"Back to scheduled tasks"}),"."]})})]})}function Pe(s){switch(s){case"error":return"bad";case"healthy":return"ok";case"running":return"info";case"disabled":return"warn";default:return}}function An(s){return Pe(s)}const Zs={error:0,running:1,idle:2,healthy:3,disabled:4,unconfigured:5};function oa(s,n){const t=Zs[s.status]-Zs[n.status];return t!==0?t:s.name.localeCompare(n.name)}const ca=s=>!!(s&&(s.processed||s.changed||s.skipped||s.failed));function Je(s){if(!ca(s)||!s)return"";const n=[];return s.processed&&n.push(`${s.processed.toLocaleString()} checked`),s.changed&&n.push(`${s.changed.toLocaleString()} changed`),s.skipped&&n.push(`${s.skipped.toLocaleString()} skipped`),s.failed&&n.push(`${s.failed.toLocaleString()} failed`),n.join(" · ")}function Ms(s){if(s.status==="running")return"Running…";if(s.error)return s.error;if(s.detail)return s.detail;const n=Je(s.counts);return n||"No changes"}function da(s){return s.powers.length?`Switching ${s.name} off also stops: ${s.powers.join("; ")}.`:""}const ha=2e4,ua=3e3;function ma(){const[s,n]=v.useState(!1),{data:t,error:i,loading:a}=J("/admin/api/integrations/services?limit=40",{pollMs:s?ua:ha}),l=((t==null?void 0:t.services)??[]).some(u=>u.running);l!==s&&n(l);const r=[...(t==null?void 0:t.services)??[]].sort(oa),d=(t==null?void 0:t.runs)??[],c=r.filter(u=>u.status==="error"),o=r.filter(u=>u.running),h=r.filter(u=>u.configured&&!u.enabled),x=r.filter(u=>!u.configured);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Integrations",intro:"The services Memby depends on: whether each one is configured, whether it is working, what it is doing right now and what it last managed to do."}),e.jsx(_,{message:i}),a?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Integrations",value:w(r.length),icon:"plug",tone:"info"},{label:"Not working",value:w(c.length),icon:"alert",tone:c.length>0?"bad":void 0},{label:"Running now",value:w(o.length),icon:"pulse",tone:o.length>0?"ok":void 0},{label:"Switched off",value:w(h.length),icon:"power",tone:h.length>0?"warn":void 0},{label:"Not configured",value:w(x.length),icon:"sliders",tone:x.length>0?"note":void 0}]}),c.length>0?e.jsxs(pe,{tone:"bad",children:[c.map(u=>u.name).join(", ")," ",c.length===1?"is not answering":"are not answering",". A failed run also publishes an administrative event, so it is in the activity feed and wherever your webhooks send it — you did not have to be looking at this page."]}):null,e.jsx(M,{title:"Services",intro:"Anything wrong sorts to the top. Open a service to switch it off, read its settings, or see everything it has done.",icon:"plug",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Integration"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Enabled"})]})}),e.jsx("tbody",{children:r.length===0?e.jsx(Y,{columns:6,children:"This gateway has no integrations."}):r.map(u=>e.jsx(pa,{service:u},u.id))})]})})}),e.jsx(M,{title:"Recent activity",intro:"Every integration's work together and in order, which is what shows two of them getting in each other's way. This is what Memby attempted and what came of it — the Logs page is where the technical detail behind a failure lives.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Integration"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:d.length===0?e.jsx(Y,{columns:6,children:"No integration has run yet."}):d.map(u=>{const g=r.find(j=>j.id===u.integrationId),b=g==null?void 0:g.tasks.find(j=>j.id===u.taskId);return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(u.startedAt),children:fe(u.startedAt)}),e.jsx("td",{className:"nowrap",children:e.jsx(Z,{className:"table-row-link",to:`/admin/integrations/${encodeURIComponent(u.integrationId??"")}`,children:(g==null?void 0:g.name)??u.integrationId})}),e.jsx("td",{className:"muted",children:(b==null?void 0:b.name)??u.taskId}),e.jsx("td",{children:e.jsx(N,{tone:us(u.status),children:u.status})}),e.jsxs("td",{className:"muted",children:[Ms(u),u.error&&Je(u.counts)?e.jsx("span",{className:"table-sub",children:Je(u.counts)}):null]}),e.jsx("td",{className:"num muted",children:ye(u.durationMs)})]},u.id)})})]})})})]})]})}function pa({service:s}){const n=`/admin/integrations/${encodeURIComponent(s.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(Z,{className:"table-row-link",to:n,children:s.name}),e.jsx("span",{className:"table-sub",children:s.address||s.summary})]}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":An(s.status)}),e.jsx(N,{tone:Pe(s.status),children:e.jsx("span",{title:s.detail||s.statusLabel,children:s.statusLabel})})]})}),e.jsx("td",{className:"nowrap muted",title:s.lastRun?F(s.lastRun.startedAt):void 0,children:s.lastRun?fe(s.lastRun.startedAt):"never"}),e.jsx("td",{className:"muted",children:s.lastRun?Ms(s.lastRun):s.detail||"—"}),e.jsx("td",{className:"nowrap muted",children:s.configured?s.enabled?s.running?"now":ds(s.nextRun):"off":"—"}),e.jsx("td",{className:"nowrap",children:s.configured?s.enabled?e.jsx(N,{tone:"ok",children:"On"}):e.jsx(N,{tone:"warn",children:"Off"}):e.jsx(N,{children:"not configured"})})]})}const xa={imdb:"IMDb",tomatoes:"Rotten Tomatoes",audience:"Rotten Tomatoes Audience",metacritic:"Metacritic",letterboxd:"Letterboxd",rogerebert:"Roger Ebert",tmdb:"TMDb",trakt:"Trakt",mal:"MyAnimeList",anilist:"AniList",anidb:"AniDB",kitsu:"Kitsu",score:"MDBList Score",score_average:"MDBList Average"};function ja(){const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(!1),[o,h]=v.useState(""),[x,u]=v.useState(!1),[g,b]=v.useState([]),[j,m]=v.useState(!1),y=s==null?void 0:s.mdblist;v.useEffect(()=>{j||!y||(c(y.enabled),b(y.sources??[]))},[y,j]);const f=()=>r("save",async()=>{await a(()=>q.post("/admin/api/mdblist-settings",{enabled:d,apiKey:o.trim(),clearApiKey:x,sources:g}),"Ratings settings saved."),h(""),u(!1),m(!1),await i()}),p=(y==null?void 0:y.cachedTitles)??0;return e.jsxs(e.Fragment,{children:[e.jsx(_,{message:n}),t||!y?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsxs(xe,{cols:"2",children:[e.jsxs(M,{title:"MDBList connection",intro:"The key stays on this server and a failure never blocks a television. Every rating fetched is stored here permanently and re-checked about once a month, so browsing the library costs nothing after the first look at a title.",icon:"star",tone:"note",actions:d?e.jsxs(N,{tone:"ok",children:["on · ",g.length," sources"]}):e.jsx(N,{children:y.apiKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(G,{label:"Show external ratings on televisions",hint:"Off leaves the stored ratings in place.",checked:d,onChange:R=>{c(R),m(!0)}}),e.jsx(D,{label:"API key",hint:"Leave blank to keep the key that is already saved.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:o,placeholder:y.apiKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:R=>h(R.target.value)})}),e.jsx(G,{label:"Remove the saved key",checked:x,onChange:u})]}),e.jsx(M,{title:"Sources shown on televisions",intro:"A title with none of these has no ratings strip at all, which is the honest answer — nothing stands in for a score that was never fetched.",icon:"list",tone:"data",children:(y.availableSources??[]).length===0?e.jsx(ee,{children:"No rating sources are available."}):e.jsx("div",{className:"checks columns",children:(y.availableSources??[]).map(R=>e.jsx(G,{label:xa[R]??R,checked:g.includes(R),onChange:k=>{m(!0),b($=>k?[...$,R]:$.filter(I=>I!==R))}},R))})})]}),e.jsx(M,{children:e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>void f(),children:"Save ratings settings"}),e.jsx("span",{className:"hint",children:p?"Ratings are fetched as televisions browse, never on the request path.":"No ratings stored yet. They are saved as televisions browse the library."})]})})]})]})}const ga={id:"",name:"Discord",url:"",enabled:!0,events:[]};function va(){const{wrap:s,show:n}=ie(),{busy:t,run:i}=ne(),{data:a,error:l,loading:r,reload:d}=J("/admin/api/integrations",{pollMs:6e4}),[c,o]=v.useState(null),[h,x]=v.useState(null),u=(a==null?void 0:a.catalogue)??[],g=(a==null?void 0:a.integrations)??[],b=f=>o({id:f.id,name:f.name,url:"",enabled:f.enabled,events:f.events??[]}),j=()=>i("save",async()=>{if(!c)return;await s(()=>q.post("/admin/api/integrations",c),c.id?"Integration saved.":"Integration added.")&&(o(null),await d())}),m=f=>i("remove",async()=>{await s(()=>q.del(`/admin/api/integrations/${encodeURIComponent(f.id)}`),`${f.name} removed.`),x(null),await d()}),y=f=>i(`test:${f.id}`,async()=>{const p=await s(()=>q.post(`/admin/api/integrations/${encodeURIComponent(f.id)}/test`));p&&n(p.message,p.ok?"ok":"bad"),await d()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Event webhooks",intro:"Send administrative events to somewhere you already look. Events pass through the gateway's own event layer, so nothing about authentication or scheduled tasks knows Discord exists — and a second kind of destination is a change here rather than everywhere.",actions:e.jsx(T,{variant:"primary",icon:"plus",onClick:()=>o(ga),children:"Add a webhook"})}),e.jsx(_,{message:l}),((a==null?void 0:a.dropped)??0)>0?e.jsxs(pe,{tone:"warn",children:[w((a==null?void 0:a.dropped)??0)," events could not be queued for delivery. The queue is deliberately lossy — a slow endpoint must never hold up a television signing in — but a number growing here means a destination is not keeping up."]}):null,r?e.jsx(V,{}):g.length===0&&!c?e.jsx(M,{title:"Nothing configured",icon:"plug",tone:"note",children:e.jsx(ee,{children:"No destinations yet. A Discord webhook takes about a minute: in Discord, open a channel's settings → Integrations → Webhooks → New Webhook, copy its URL, and paste it here."})}):g.map(f=>e.jsx(ya,{integration:f,catalogue:u,busy:t,onEdit:()=>b(f),onTest:()=>void y(f),onRemove:()=>x(f)},f.id)),c?e.jsx(wa,{draft:c,catalogue:u,busy:t==="save",onChange:o,onSave:()=>void j(),onCancel:()=>o(null)}):null,h?e.jsx(ke,{title:`Remove ${h.name}?`,body:"The webhook address and its delivery history go with it. Events already published stay in the activity feed.",confirmLabel:"Remove",destructive:!0,busy:t==="remove",onConfirm:()=>void m(h),onCancel:()=>x(null)}):null]})}function fa(){const{wrap:s}=ie(),{busy:n,run:t}=ne(),{data:i,error:a,loading:l,reload:r}=J("/admin/api/sonarr-request-policy"),[d,c]=v.useState(0),[o,h]=v.useState(!1);v.useEffect(()=>{i&&(c(i.qualityProfileId),h(i.searchImmediately))},[i]);const x=()=>t("sonarr-request-policy",async()=>{await s(()=>q.post("/admin/api/sonarr-request-policy",{qualityProfileId:d,searchImmediately:o}),"Sonarr TV request policy saved."),await r()}),u=i==null?void 0:i.profiles.find(g=>g.id===d);return e.jsxs(M,{title:"Sonarr TV requests",intro:"The policy Memby uses when a viewer requests a television series. The series remains monitored; searching its existing episodes is an explicit choice.",icon:"tv",tone:i!=null&&i.configured?"ok":"warn",actions:i!=null&&i.configured?e.jsx(N,{tone:"ok",children:"configured"}):e.jsx(N,{tone:"warn",children:"needs attention"}),footer:e.jsx(T,{variant:"primary",busy:n==="sonarr-request-policy",disabled:l||d<=0,onClick:()=>void x(),children:"Save Sonarr policy"}),children:[e.jsx(_,{message:a??(i==null?void 0:i.error)??""}),l?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Request quality profile",hint:"Memby stores this Sonarr profile ID. 720p is the recommended safe default; Memby will never fall back to Any.",children:e.jsxs("select",{value:d,onChange:g=>c(Number(g.target.value)),disabled:!(i!=null&&i.profiles.length),children:[e.jsx("option",{value:0,children:"Choose a quality profile…"}),i==null?void 0:i.profiles.map(g=>e.jsxs("option",{value:g.id,children:[g.name,g.recommended?" — recommended (720p)":""]},g.id))]})})}),e.jsx(G,{label:"Search for episodes immediately after request",hint:"Off adds and monitors the series without searching its backlog. Enable only when requests should start an immediate episode search.",checked:o,onChange:h}),u?e.jsxs(pe,{tone:"info",children:["Requested series will use ",e.jsx("b",{children:u.name})," (profile ID ",u.id,"), be monitored using Memby’s existing all-episodes strategy, and ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function ba(){const{wrap:s}=ie(),{busy:n,run:t}=ne(),{data:i,error:a,loading:l,reload:r}=J("/admin/api/radarr-request-policy"),[d,c]=v.useState(0),[o,h]=v.useState(!1);v.useEffect(()=>{i&&(c(i.qualityProfileId),h(i.searchImmediately))},[i]);const x=()=>t("radarr-request-policy",async()=>{await s(()=>q.post("/admin/api/radarr-request-policy",{qualityProfileId:d,searchImmediately:o}),"Radarr movie request policy saved."),await r()}),u=i==null?void 0:i.profiles.find(g=>g.id===d);return e.jsxs(M,{title:"Radarr movie requests",intro:"The policy Memby uses when a viewer requests a film. The film remains monitored; an immediate Radarr search is an explicit choice.",icon:"tv",tone:i!=null&&i.configured?"ok":"warn",actions:i!=null&&i.configured?e.jsx(N,{tone:"ok",children:"configured"}):e.jsx(N,{tone:"warn",children:"needs attention"}),footer:e.jsx(T,{variant:"primary",busy:n==="radarr-request-policy",disabled:l||d<=0,onClick:()=>void x(),children:"Save Radarr policy"}),children:[e.jsx(_,{message:a??(i==null?void 0:i.error)??""}),l?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Request quality profile",hint:"Memby stores this Radarr profile ID. 720p is the recommended safe default; Memby will never fall back to Any.",children:e.jsxs("select",{value:d,onChange:g=>c(Number(g.target.value)),disabled:!(i!=null&&i.profiles.length),children:[e.jsx("option",{value:0,children:"Choose a quality profile…"}),i==null?void 0:i.profiles.map(g=>e.jsxs("option",{value:g.id,children:[g.name,g.recommended?" — recommended (720p)":""]},g.id))]})})}),e.jsx(G,{label:"Search for the film immediately after request",hint:"Off adds and monitors the film without asking Radarr to search. Enable only when requests should start an immediate search.",checked:o,onChange:h}),u?e.jsxs(pe,{tone:"info",children:["Requested films will use ",e.jsx("b",{children:u.name})," (profile ID ",u.id,"), remain monitored, and ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function ya({integration:s,catalogue:n,busy:t,onEdit:i,onTest:a,onRemove:l}){const r=s.health,d=!r.lastFailure||r.lastSuccess&&r.lastSuccess>r.lastFailure,c=s.events??[];return e.jsxs(M,{title:s.name,intro:s.hint?`Discord webhook ${s.hint}`:"Discord webhook",icon:"plug",tone:s.enabled?"ok":"warn",actions:e.jsxs(e.Fragment,{children:[s.enabled?e.jsx(N,{tone:"ok",children:"on"}):e.jsx(N,{tone:"warn",children:"off"}),r.deliveries>0?e.jsx(N,{tone:d?"ok":"bad",children:d?"delivering":"failing"}):e.jsx(N,{children:"never used"}),e.jsx(T,{size:"sm",icon:"pulse",busy:t===`test:${s.id}`,onClick:a,children:"Test"}),e.jsx(T,{size:"sm",onClick:i,children:"Edit"}),e.jsx(T,{size:"sm",variant:"danger",icon:"trash",onClick:l,title:"Remove"})]}),children:[e.jsxs("div",{className:"list",children:[e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Events sent"}),e.jsx("p",{children:c.length===0?"None selected — this destination is configured but will never post anything.":c.map(o=>{var h;return((h=n.find(x=>x.type===o))==null?void 0:h.label)??o}).join(", ")})]})}),e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last delivered"}),e.jsx("p",{children:r.lastSuccess?F(r.lastSuccess):"never"})]}),e.jsx("div",{className:"list-actions",children:r.deliveries>0?e.jsxs("span",{className:"quiet",children:[w(r.deliveries)," attempts, ",w(r.failures)," failed"]}):null})]}),r.lastFailure?e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last failure"}),e.jsxs("p",{children:[F(r.lastFailure),r.lastError?` — ${r.lastError}`:""]})]})}):null]}),s.deliveries.length>0?e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Attempted"}),e.jsx("th",{children:"Event"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:s.deliveries.map(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(o.attemptedAt),children:fe(o.attemptedAt)}),e.jsx("td",{className:"muted",children:o.eventType}),e.jsx("td",{children:o.success?e.jsx(N,{tone:"ok",children:o.statusCode||"ok"}):e.jsx(N,{tone:"bad",children:o.error||o.statusCode||"failed"})}),e.jsx("td",{className:"num muted",children:ye(o.durationMs)})]},o.id))})]})}):e.jsx(K,{children:e.jsx("table",{children:e.jsx("tbody",{children:e.jsx(Y,{columns:4,children:"Nothing has been delivered through this webhook yet."})})})})]})}function wa({draft:s,catalogue:n,busy:t,onChange:i,onSave:a,onCancel:l}){const r=[...new Set(n.map(c=>c.group))],d=(c,o)=>i({...s,events:o?[...s.events,c]:s.events.filter(h=>h!==c)});return e.jsxs(M,{title:s.id?`Edit ${s.name}`:"New Discord webhook",icon:"plug",tone:"info",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:t,onClick:a,children:s.id?"Save":"Add"}),e.jsx(T,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx("span",{className:"spacer"}),s.events.length===0?e.jsx("span",{className:"quiet",children:"Nothing selected — this destination would never post."}):e.jsxs("span",{className:"quiet",children:[s.events.length," events selected"]})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Name",hint:"What this destination is called in the console.",children:e.jsx("input",{type:"text",value:s.name,onChange:c=>i({...s,name:c.target.value})})}),e.jsx(D,{label:"Webhook address",hint:s.id?"Leave blank to keep the address already saved — it is a credential and is never sent back to this page.":"Discord → channel settings → Integrations → Webhooks → New Webhook → Copy Webhook URL.",children:e.jsx("input",{type:"url",value:s.url,placeholder:s.id?"unchanged":"https://discord.com/api/webhooks/…",onChange:c=>i({...s,url:c.target.value})})})]}),e.jsx(G,{label:"Enabled",hint:"Off keeps the configuration and stops the posts.",checked:s.enabled,onChange:c=>i({...s,enabled:c})}),r.map(c=>e.jsxs("div",{children:[e.jsx("div",{className:"card-head",style:void 0,children:e.jsx("div",{className:"card-head-text",children:e.jsx("h2",{children:c})})}),n.filter(o=>o.group===c).map(o=>e.jsx(G,{label:o.label,hint:o.description,checked:s.events.includes(o.type),onChange:h=>d(o.type,h)},o.type))]},c))]})}const ka=2e4,Na=3e3;function Sa(){const{integrationId:s=""}=Be(),{wrap:n,show:t}=ie(),{busy:i,run:a}=ne(),[l,r]=v.useState(!1),{data:d,error:c,loading:o,reload:h}=J(`/admin/api/integrations/services/${encodeURIComponent(s)}?limit=80`,{pollMs:l?Na:ka,enabled:!!s}),x=d==null?void 0:d.service,u=(d==null?void 0:d.runs)??[],g=!!(x!=null&&x.running);g!==l&&r(g);const b=f=>a("enabled",async()=>{await n(()=>q.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/enabled`,{enabled:f}),`${(x==null?void 0:x.name)??"Integration"} ${f?"enabled":"disabled"}.`),await h()}),j=()=>a("test",async()=>{const f=await n(()=>q.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/test`));f&&t(f.reachable?`${x==null?void 0:x.name} answered in ${ye(f.latencyMs)}.`:f.error||`${x==null?void 0:x.name} did not answer.`,f.reachable?"ok":"bad"),await h()}),m=(f,p)=>a(f,async()=>{await n(()=>q.post(`/admin/api/tasks/${encodeURIComponent(f)}/run`),`${p} started.`),await h()});if(o||!x)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Integration",intro:"One external service and everything it has done."}),e.jsx(_,{message:c}),o?e.jsx(V,{}):e.jsx(pe,{tone:"warn",children:"No such integration."})]});const y=u.filter(f=>f.status==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:x.name,intro:x.summary,actions:e.jsx(Z,{className:"table-row-link",to:"/admin/integrations",children:"All integrations"})}),e.jsx(_,{message:c}),e.jsx(ae,{tiles:[{label:"Status",value:x.statusLabel,small:!0,icon:"pulse",tone:Pe(x.status)},{label:"Runs recorded",value:w(x.runs),icon:"history",tone:"data"},{label:"Failed runs",value:w(x.failures),icon:"alert",tone:x.failures>0?"bad":void 0},{label:"Last worked",value:x.lastSuccessAt?fe(x.lastSuccessAt):"never",small:!0,icon:"check",tone:x.lastSuccessAt?"ok":void 0}]}),e.jsxs(M,{title:"Connection",intro:"Address and credentials come from this gateway's environment; the switch is stored on the server and applies to the whole household.",icon:"plug",tone:Pe(x.status)??"info",actions:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":An(x.status)}),e.jsx(N,{tone:Pe(x.status),children:x.statusLabel})]}),footer:x.configured&&x.probed?e.jsx(T,{icon:"sync",busy:i==="test",onClick:()=>void j(),children:"Test connection"}):void 0,children:[x.detail?e.jsx(pe,{tone:Pe(x.status),children:x.detail}):null,x.configured?e.jsxs(e.Fragment,{children:[e.jsx(G,{label:`${x.name} enabled`,hint:x.enabled?da(x):`Memby is not calling ${x.name} or scheduling any of its work.`,checked:x.enabled,disabled:i==="enabled",onChange:f=>void b(f)}),e.jsx(We,{rows:[{label:"Address",value:x.address||"—"},...(x.facts??[]).map(f=>({label:f.label,value:f.tone?e.jsx(N,{tone:f.tone==="warn"?"warn":"ok",children:f.value}):f.value})),{label:"Last checked",value:x.probed?x.health?`${fe(x.health.checkedAt)} · ${ye(x.health.latencyMs)}`:"not yet":"Not probed — see below"}]}),x.probed?null:e.jsxs(pe,{children:[x.name," is not probed for reachability: its allowance is bought by the day, and spending a request of it to draw a status would compete with the televisions for the thing being reported on. Its health comes from the run history below."]})]}):e.jsxs(pe,{tone:"warn",children:["This gateway has no address or credential for ",x.name,". Set them in the deployment's environment and restart the container; there is nothing to switch until then."]})]}),x.powers.length>0?e.jsx(M,{title:"What depends on this",intro:"Switching the service off stops all of it. Nothing below fails quietly — it stops being offered.",icon:"journey",tone:"note",children:e.jsx("div",{className:"list",children:x.powers.map(f=>e.jsxs("div",{className:"list-item",children:[e.jsx(Qe,{name:"check",tone:"ok"}),e.jsx("span",{className:"list-body",children:f})]},f))})}):null,e.jsx(Ma,{id:x.id}),e.jsx(M,{title:"Scheduled work",intro:"The background jobs belonging to this service. Cadence and the per-job switch live on each job's own page; this is where you start one by hand.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:x.tasks.length===0?e.jsx(Y,{columns:5,children:"This service has no scheduled work: Memby calls it when a television asks for something rather than on a timer."}):x.tasks.map(f=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(Z,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(f.id)}`,children:f.name}),e.jsx("span",{className:"table-sub",children:f.description})]}),e.jsx("td",{className:"nowrap muted",children:Ne(f.intervalSeconds)}),e.jsx("td",{className:"nowrap muted",title:f.lastRun?F(f.lastRun.startedAt):void 0,children:f.lastRun?fe(f.lastRun.startedAt):"never"}),e.jsx("td",{className:"nowrap muted",children:!f.enabled||!x.enabled?"off":f.running?"now":ds(f.nextRun)}),e.jsx("td",{className:"nowrap",children:e.jsx(T,{size:"sm",icon:"play",busy:i===f.id,disabled:f.running,title:`Run ${f.name} now`,onClick:()=>void m(f.id,f.name)})})]},f.id))})]})})}),e.jsx(M,{title:"Run history",intro:"What Memby attempted on this service's behalf and what came of it. This is not the application log — a failure here gives the reason, and Logs is where the technical detail behind it lives.",icon:"history",tone:"note",actions:y>0?e.jsx(Z,{className:"table-row-link",to:`/admin/logs?q=${encodeURIComponent(x.id)}`,children:"Open the logs"}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Checked"}),e.jsx("th",{className:"num",children:"Changed"}),e.jsx("th",{className:"num",children:"Skipped"}),e.jsx("th",{className:"num",children:"Failed"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:u.length===0?e.jsxs(Y,{columns:10,children:["Nothing has run for ",x.name," yet."]}):u.map(f=>e.jsx(Ca,{run:f,service:x},f.id))})]})})})]})}function Ca({run:s,service:n}){var a,l,r,d;const t=n.tasks.find(c=>c.id===s.taskId),i=c=>c?w(c):"—";return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(s.startedAt),children:fe(s.startedAt)}),e.jsx("td",{className:"muted",children:(t==null?void 0:t.name)??s.taskId}),e.jsx("td",{className:"muted",children:s.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(s.status),children:s.status})}),e.jsxs("td",{className:"muted",children:[Ms(s),s.error&&Je(s.counts)?e.jsx("span",{className:"table-sub",children:Je(s.counts)}):null]}),e.jsx("td",{className:"num muted",children:i(((a=s.counts)==null?void 0:a.processed)??0)}),e.jsx("td",{className:"num muted",children:i(((l=s.counts)==null?void 0:l.changed)??0)}),e.jsx("td",{className:"num muted",children:i(((r=s.counts)==null?void 0:r.skipped)??0)}),e.jsx("td",{className:"num muted",children:i(((d=s.counts)==null?void 0:d.failed)??0)}),e.jsx("td",{className:"num muted",children:ye(s.durationMs)})]})}function Ma({id:s}){switch(s){case"mdblist":return e.jsx(ja,{});case"sonarr":return e.jsx(fa,{});case"radarr":return e.jsx(ba,{});default:return null}}function Ta(){var X,le,ue,te;const{status:s,error:n,loading:t,reload:i}=je(),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(""),[o,h]=v.useState(!1),[x,u]=v.useState(!1),[g,b]=v.useState(!1),[j,m]=v.useState("23:00"),[y,f]=v.useState("07:00"),[p,R]=v.useState(""),[k,$]=v.useState(!1),I=!!((X=s==null?void 0:s.maintenance)!=null&&X.enabled);v.useEffect(()=>{var H;!x&&s&&c(((H=s.maintenance)==null?void 0:H.message)??"")},[s,x]),v.useEffect(()=>{k||!(s!=null&&s.quietTime)||(b(s.quietTime.enabled),m(s.quietTime.startTime),f(s.quietTime.endTime),R(s.quietTime.message))},[s,k]);const A=H=>r(H?"on":"off",async()=>{await a(()=>q.post("/admin/api/maintenance",{enabled:H,message:d}),H?"Memby is offline for every television.":"Memby is back online."),h(!1),u(!1),await i()}),L=()=>r("quiet",async()=>{await a(()=>q.post("/admin/api/quiet-time",{enabled:g,startTime:j,endTime:y,message:p}),g?"Quiet time saved.":"Quiet time turned off."),$(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time."}),e.jsx(_,{message:n}),t?e.jsx(V,{rows:1}):e.jsx(M,{title:"Gateway availability",intro:"Takes Memby offline for every television, independently of Emby. Sign-in and all content calls answer 503 with the message below, and the television shows it in place of the launcher rows. This console keeps working.",icon:"power",tone:I?"bad":"warn",actions:I?e.jsx(N,{tone:"bad",children:"offline"}):e.jsx(N,{tone:"ok",children:"online"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"danger",disabled:I,onClick:()=>h(!0),children:"Go offline"}),e.jsx(T,{disabled:!I,busy:l==="off",onClick:()=>void A(!1),children:"Bring back online"})]}),children:e.jsx(D,{label:"Message shown on the television",hint:"Say what is happening and when it will be back. It is the only thing the viewer is told.",children:e.jsx("input",{type:"text",value:d,placeholder:"Back shortly — upgrading the server",onChange:H=>{c(H.target.value),u(!0)}})})}),t?null:e.jsxs(M,{title:"Quiet time",intro:`Pause new television requests and server background work every day in ${((le=s==null?void 0:s.quietTime)==null?void 0:le.timeZone)??"the server timezone"}. Work already under way finishes safely. The admin console and health checks stay available so the schedule can always be changed.`,icon:"clock",tone:(ue=s==null?void 0:s.quietTime)!=null&&ue.active?"warn":"info",actions:(te=s==null?void 0:s.quietTime)!=null&&te.active?e.jsx(N,{tone:"warn",children:"active now"}):g?e.jsx(N,{tone:"ok",children:"scheduled"}):e.jsx(N,{children:"off"}),footer:e.jsx(T,{variant:"primary",busy:l==="quiet",onClick:()=>void L(),children:"Save quiet time"}),children:[e.jsx(G,{label:"Pause server activity during quiet time",checked:g,onChange:H=>{b(H),$(!0)}}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts",hint:"Uses the server's 24-hour clock.",children:e.jsx("input",{type:"time",value:j,onChange:H=>{m(H.target.value),$(!0)}})}),e.jsx(D,{label:"Ends",hint:"May be on the following day, for example 23:00 to 07:00.",children:e.jsx("input",{type:"time",value:y,onChange:H=>{f(H.target.value),$(!0)}})})]}),e.jsx(D,{label:"Message shown on the television",hint:"Shown when a television contacts Memby during quiet time.",children:e.jsx("input",{type:"text",value:p,placeholder:"Quiet time — try again after 7 am",onChange:H=>{R(H.target.value),$(!0)}})})]}),o?e.jsx(ke,{title:"Take Memby offline?",body:"Every television will stop working immediately and show your message in place of the launcher. This console keeps working.",confirmLabel:"Go offline",destructive:!0,busy:l==="on",onConfirm:()=>void A(!0),onCancel:()=>h(!1)}):null]})}const Ys=s=>s==="Unattributed"?"warn":s==="Go runtime"?"note":"info";function Ra(){const s=J("/admin/api/runtime",{pollMs:3e4}),[n,t]=v.useState(),[i,a]=v.useState(!1),[l,r]=v.useState(""),d=v.useCallback(async()=>{a(!0),r("");try{t(await q.get("/admin/api/runtime/goroutines"))}catch(b){r(b instanceof Error?b.message:String(b))}finally{a(!1)}},[]);if(s.loading||!s.data)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Runtime",intro:"What the gateway process is doing."}),e.jsx(_,{message:s.error}),e.jsx(V,{})]});const c=s.data,o=c.memory,h=c.samples??[],x=c.workers??[],u=x.filter(b=>b.state==="running").length,g=o.memoryLimit>0&&o.memoryLimitw(Math.round(b)))}`,value:w(c.goroutines),icon:"pulse",tone:c.goroutineTrend.direction==="rising"?"warn":"info"},{label:"named workers running",value:w(u),icon:"clock",tone:"note"},{label:`heap in use · ${rs(c.heapTrend,he)}`,value:he(o.heapInuse),icon:"chip",tone:o.heapShare>=.75?"warn":"data"},{label:"running for",value:En(c.uptimeSeconds),icon:"history",tone:"data"}]}),e.jsxs(xe,{cols:"2",children:[e.jsxs(M,{title:"Memory",intro:"Three different figures that are routinely confused. Heap in use is what the gateway is actually holding. Reserved is what the Go runtime has taken from the operating system on its behalf — always larger, and not a leak. The limit is the one the container is stopped at.",icon:"chip",tone:"data",children:[g?e.jsxs(e.Fragment,{children:[e.jsx(Tn,{value:o.heapInuse,total:o.memoryLimit,tone:o.heapShare>=.9?"bad":o.heapShare>=.75?"warn":"ok"}),e.jsxs("p",{className:"hint",children:["The heap is using ",(o.heapShare*100).toFixed(1),"% of the"," ",he(o.memoryLimit)," limit",o.configuredLimit?` set by GOMEMLIMIT=${o.configuredLimit}`:"","."]})]}):e.jsx(pe,{tone:"warn",children:"No memory limit is set, so the Go runtime will grow until the container is killed by the host. Set GOMEMLIMIT to give it a ceiling to collect against."}),e.jsx(We,{rows:[{label:"Heap currently used",value:he(o.heapInuse)},{label:"Heap allocated to live objects",value:he(o.heapAlloc)},{label:"Heap held but idle",value:he(o.heapIdle)},{label:"Returned to the operating system",value:he(o.heapReleased)},{label:"Goroutine stacks",value:he(o.stackInuse)},{label:"Runtime / system reserved",value:he(o.sys)},{label:"Configured process limit",value:g?he(o.memoryLimit):"none"},{label:"Next collection at",value:he(o.nextGc)}]})]}),e.jsxs(M,{title:"Collection",intro:"How often the garbage collector runs and how long it stops the gateway for. Everything the televisions ask for waits during a pause, so this is the figure that turns a memory problem into a slow one.",icon:"refresh",tone:"note",children:[e.jsx(Cs,{tiles:[{label:"collections",value:w(o.numGc)},{label:"collections an hour",value:o.gcPerHour>0?o.gcPerHour.toFixed(0):"—"},{label:"recent pause",value:`${o.pauseRecentMs.toFixed(1)} ms`},{label:"paused in total",value:`${(o.pauseTotalMs/1e3).toFixed(1)} s`}]}),e.jsx(We,{rows:[{label:"Processors available",value:w(c.gomaxprocs)},{label:"Operating-system threads",value:w(c.threads)},{label:"Processor use",value:c.process.cpuKnown?`${(c.process.cpuPercent??0).toFixed(1)}% of one processor · ${(c.process.cpuSeconds??0).toFixed(0)}s used in total`:"not available on this host"},{label:"Open connections",value:c.process.filesKnown?`${w(c.process.openSockets)} sockets of ${w(c.process.openFiles)} open files${c.process.fileLimit?` · limit ${w(c.process.fileLimit)}`:""}`:"not available on this host"},{label:"Go version",value:c.goVersion}]})]})]}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"Goroutines over time",intro:"A single instantaneous count cannot show a leak. This can: a line that climbs and never comes back down is work the gateway is not letting go of.",icon:"chart",tone:"info",children:e.jsx(Js,{samples:h,valueOf:b=>b.goroutines,format:b=>w(b),everySeconds:c.sampleEverySeconds})}),e.jsx(M,{title:"Memory over time",intro:"Heap in use, sampled on the same tick. A saw-tooth that returns to roughly the same floor after each collection is healthy; a floor that keeps rising is not.",icon:"chart",tone:"data",children:e.jsx(Js,{samples:h,valueOf:b=>b.heapInuse,format:he,everySeconds:c.sampleEverySeconds})})]}),e.jsx(M,{title:"Background workers",intro:"Memby's own long-running work, named where it is started rather than guessed at from a stack. A worker that has finished is not necessarily a fault — several are one-shot startup jobs — but one that keeps being started again is failing at something.",icon:"clock",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Worker"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Since"}),e.jsx("th",{className:"num",children:"Starts"})]})}),e.jsx("tbody",{children:x.length===0?e.jsx(Y,{columns:5,children:"Nothing has registered. This gateway predates named workers."}):x.map(b=>e.jsx($a,{worker:b},b.name))})]})})}),e.jsxs(M,{title:"Goroutine breakdown",intro:"What those goroutines are actually doing, and which part of Memby they belong to. Collecting it means walking every stack, which stops the gateway for a few milliseconds — so it is taken when you ask rather than continuously.",icon:"search",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx(T,{onClick:d,busy:i,icon:"refresh",variant:"primary",children:n?"Collect again":"Collect breakdown"}),e.jsx("a",{href:"/admin/api/runtime/goroutines?format=text",download:!0,children:e.jsx(T,{icon:"download",variant:"quiet",children:"Download full stack dump"})})]}),e.jsx(_,{message:l}),n?e.jsx(Ea,{report:n}):e.jsx(ee,{children:"Nothing collected yet. The snapshot is a moment in time, so it is taken on request and stamped with when it was taken."})]})]})}function $a({worker:s}){const n=s.state==="finished";return e.jsxs("tr",{children:[e.jsx("td",{children:s.name}),e.jsx("td",{children:e.jsx(N,{tone:"info",children:s.component})}),e.jsx("td",{children:e.jsx(N,{tone:n?"note":"ok",children:n?"finished":"running"})}),e.jsx("td",{children:F(n?s.stopped:s.started)}),e.jsx("td",{className:"num",children:s.starts>1?e.jsx(N,{tone:"warn",children:s.starts}):s.starts})]})}function Js({samples:s,valueOf:n,format:t,everySeconds:i}){if(s.length<2)return e.jsxs(ee,{children:["Not enough history yet. Readings are taken every"," ",i>=60?`${Math.round(i/60)} minutes`:`${i}s`,", and a trend needs about fifteen minutes of them."]});const a=l=>new Date(l.at).toLocaleTimeString();return e.jsx(hs,{data:s,labelOf:l=>a(l),valueOf:l=>n(l),title:l=>`${a(l)}: ${t(n(l))}`})}function Ea({report:s}){const n=s.categories??[],t=s.components??[],i=s.groups??[];return e.jsxs(e.Fragment,{children:[e.jsxs("p",{className:"hint",children:[w(s.total)," goroutines, collected ",F(s.at)," in"," ",s.collectedInMs.toFixed(1)," ms."]}),e.jsxs(xe,{cols:"2",children:[e.jsxs("div",{children:[e.jsx(xs,{children:"By what they are doing"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"State"}),e.jsx("th",{className:"num",children:"Count"})]})}),e.jsx("tbody",{children:n.map(a=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:a.label}),e.jsx("p",{className:"hint",children:a.description}),e.jsx("p",{className:"hint",children:(a.states??[]).map(l=>`${l.state} (${l.count})`).join(" · ")})]}),e.jsx("td",{className:"num",children:w(a.count)})]},a.category))})]})})]}),e.jsxs("div",{children:[e.jsx(xs,{children:"By which part of Memby"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Area"}),e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:t.map(a=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(N,{tone:Ys(a.component),children:a.component})}),e.jsx("td",{className:"num",children:w(a.count)}),e.jsx("td",{className:"num",children:a.longestWaitMinutes>0?`${w(a.longestWaitMinutes)} min`:"—"})]},a.component))})]})})]})]}),e.jsx(xs,{aside:s.groupsTotal>i.length?`the ${i.length} largest of ${w(s.groupsTotal)}`:void 0,children:"Where they are waiting"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Function"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(Y,{columns:5,children:"Nothing to group."}):i.map((a,l)=>e.jsxs("tr",{children:[e.jsx("td",{className:"num",children:w(a.count)}),e.jsx("td",{children:e.jsx(N,{tone:Ys(a.component),children:a.component})}),e.jsx("td",{children:a.state}),e.jsxs("td",{children:[e.jsx("span",{className:"mono",children:a.function}),e.jsx("p",{className:"hint",children:a.file})]}),e.jsx("td",{className:"num",children:a.longestWaitMinutes>0?`${w(a.longestWaitMinutes)} min`:"—"})]},`${a.component}-${a.function}-${a.state}-${l}`))})]})})]})}const Aa=-1;function De(s){return s===0?"":s<0?"off":String(s)}function qe(s,n){const t=s.trim().toLowerCase();if(t==="")return 0;if(n&&(t==="off"||t==="none"||t==="0"))return Aa;const i=Number.parseInt(t,10);return Number.isFinite(i)?i:0}function Fe(s,n){return s<=0?"off":`${s} ${n}${s===1?"":"s"}`}function Qs(s){switch(s){case"everywhere":return"Everywhere";case"off":return"Off";default:return"Home only"}}function gs(s){return{timezone:s.timezone??"",logLevel:s.logLevel??"",sessionIdleDays:De(s.sessionIdleDays),sonarrAlertMinutes:De(s.sonarrAlertMinutes),radarrAlertMinutes:De(s.radarrAlertMinutes),notificationDisplay:s.notificationDisplay||"home_only",embyHealthSeconds:De(s.embyHealthSeconds),slowRequestMillis:De(s.slowRequestMillis),librarySyncMinutes:De(s.librarySyncMinutes)}}function Ia(){const{data:s,error:n,loading:t,reload:i}=J("/admin/api/gateway-settings"),{wrap:a}=ie(),{busy:l,run:r}=ne(),[d,c]=v.useState(null);v.useEffect(()=>{!d&&s&&c(gs(s.settings))},[s,d]);const o=(m,y)=>c(f=>f&&{...f,[m]:y}),h=()=>r("save",async()=>{if(!d)return;const m={timezone:d.timezone.trim(),logLevel:d.logLevel.trim(),sessionIdleDays:qe(d.sessionIdleDays,!1),sonarrAlertMinutes:qe(d.sonarrAlertMinutes,!0),radarrAlertMinutes:qe(d.radarrAlertMinutes,!0),notificationDisplay:d.notificationDisplay,embyHealthSeconds:qe(d.embyHealthSeconds,!0),slowRequestMillis:qe(d.slowRequestMillis,!0),librarySyncMinutes:qe(d.librarySyncMinutes,!0)},y=await a(()=>q.post("/admin/api/gateway-settings",m),"Gateway settings saved.");y&&c(gs(y.settings)),await i()}),x=()=>r("clear",async()=>{const m=await a(()=>q.post("/admin/api/gateway-settings",{timezone:"",logLevel:"",sessionIdleDays:0,sonarrAlertMinutes:0,radarrAlertMinutes:0,embyHealthSeconds:0,slowRequestMillis:0,librarySyncMinutes:0,notificationDisplay:"home_only"}),"Every setting is back to its deployed or server default.");m&&c(gs(m.settings)),await i()}),u=s==null?void 0:s.deployed,g=s==null?void 0:s.effective,b=(s==null?void 0:s.logLevels)??[],j=(s==null?void 0:s.notificationDisplays)??["everywhere","home_only","off"];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Gateway settings",intro:"Server-level settings for this gateway, changeable without a redeployment."}),e.jsx(_,{message:n}),t||!d||!u||!g?e.jsx(V,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"This gateway",intro:"What the process is running and what it currently believes.",icon:"chip",tone:"info",actions:e.jsx(N,{tone:"info",children:(s==null?void 0:s.version)??"unknown"}),children:e.jsx(We,{rows:[{label:"Server timezone",value:g.timezone||"not set"},{label:"Log level",value:g.logLevel},{label:"Sign-in expiry",value:Fe(g.sessionIdleDays,"day")},{label:"Emby health probe",value:Fe(g.embyHealthSeconds,"second")},{label:"Slow-request breakdown",value:Fe(g.slowRequestMillis,"millisecond")},{label:"Catalogue sweep",value:Fe(g.librarySyncMinutes,"minute")},{label:"Episode alert window",value:Fe(g.sonarrAlertMinutes,"minute")},{label:"Film alert window",value:Fe(g.radarrAlertMinutes,"minute")},{label:"Notification display",value:Qs(g.notificationDisplay)}]})}),e.jsxs(M,{title:"Server settings",intro:"Environment-backed fields can be left empty to use the deployed value shown beneath them. Changes take effect immediately — nothing here needs a restart.",icon:"sliders",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>void h(),children:"Save settings"}),e.jsx(T,{busy:l==="clear",onClick:()=>void x(),children:"Use defaults"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Server timezone",hint:`Deployed: ${u.timezone||"not set"}. An IANA name, for example Pacific/Auckland. Decides what "today" means for the schedule rows, the home hero and the sign-in history.`,children:e.jsx("input",{type:"text",value:d.timezone,placeholder:u.timezone,onChange:m=>o("timezone",m.target.value)})}),e.jsx(D,{label:"Log level",hint:`Deployed: ${u.logLevel}. Applies to the running process at once, so debug can be turned on to watch something happen.`,children:e.jsxs("select",{value:d.logLevel,onChange:m=>o("logLevel",m.target.value),children:[e.jsxs("option",{value:"",children:["Deployed (",u.logLevel,")"]}),b.map(m=>e.jsx("option",{value:m,children:m},m))]})})]}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Sign a television out after (days)",hint:`Deployed: ${u.sessionIdleDays} days. A session row holds a live Emby token, so this is how long a set nobody uses keeps working credentials.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.sessionIdleDays,placeholder:String(u.sessionIdleDays),onChange:m=>o("sessionIdleDays",m.target.value)})}),e.jsx(D,{label:"Slow-request breakdown above (ms)",hint:`Deployed: ${u.slowRequestMillis||"off"}. A request slower than this logs where its time went — Emby, Postgres, Redis, row assembly. Lower it while chasing one slow screen; type off to stop annotating altogether.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.slowRequestMillis,placeholder:String(u.slowRequestMillis),onChange:m=>o("slowRequestMillis",m.target.value)})}),e.jsx(D,{label:"Emby health probe (seconds)",hint:`Deployed: ${u.embyHealthSeconds||"off"}. How often the gateway asks Emby whether it is answering. Type off to stop probing, which also removes the outage bar from every television.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.embyHealthSeconds,placeholder:String(u.embyHealthSeconds),onChange:m=>o("embyHealthSeconds",m.target.value)})})]}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Catalogue sweep (minutes)",hint:`Deployed: ${u.librarySyncMinutes||"off"}. How often the gateway asks Emby what has changed. With the Sonarr and Radarr webhooks wired up a new file is in the catalogue within a minute of landing, and this is only reconciliation for media they do not manage — 360 is a sensible choice then. Without them it is the only way anything is found.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.librarySyncMinutes,placeholder:String(u.librarySyncMinutes),onChange:m=>o("librarySyncMinutes",m.target.value)})})}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Notification display",hint:"Controls informational banners for every viewer and television. Everywhere also permits playback overlays; Home only keeps them off active movies, episodes and trailers; Off hides them throughout Memby.",children:e.jsx("select",{value:d.notificationDisplay,onChange:m=>o("notificationDisplay",m.target.value),children:j.map(m=>e.jsx("option",{value:m,children:Qs(m)},m))})})}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Episode alert window (minutes)",hint:`Deployed: ${u.sonarrAlertMinutes||"off"}. How long a "just aired" notice stays on offer to a set that was switched off at the time. Type off to stop announcing them.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.sonarrAlertMinutes,placeholder:String(u.sonarrAlertMinutes),onChange:m=>o("sonarrAlertMinutes",m.target.value)})}),e.jsx(D,{label:"Film alert window (minutes)",hint:`Deployed: ${u.radarrAlertMinutes||"off"}. The same, for a film Radarr has just imported.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.radarrAlertMinutes,placeholder:String(u.radarrAlertMinutes),onChange:m=>o("radarrAlertMinutes",m.target.value)})})]}),e.jsxs(pe,{tone:"note",children:["These settings live in the database and survive a restart. A deployment rewrites",e.jsx("code",{children:".env"}),", not this document, so environment-backed values can then disagree; permanent environment changes belong in ",e.jsx("code",{children:".env.example"})," as well."]}),s!=null&&s.settings.updatedBy?e.jsxs(pe,{children:["Last changed by ",s.settings.updatedBy,s.settings.updatedAt?` on ${new Date(s.settings.updatedAt).toLocaleString("en-NZ")}`:"","."]}):null]})]})]})}const La={done:"ok",pending:"warn",failed:"bad"};function Da(s){const n=s.payload??{};if(n.series){const t=n.episode&&n.episode>0?` S${String(n.season??0).padStart(2,"0")}E${String(n.episode).padStart(2,"0")}`:"";return`${n.series}${t}`}return n.title?n.year?`${n.title} (${n.year})`:n.title:s.key}function qa(){const{data:s,error:n,loading:t}=J("/admin/api/ingest",{pollMs:15e3});if(t)return e.jsx(V,{rows:1});const i=!!(s!=null&&s.sonarrConfigured||s!=null&&s.radarrConfigured),a=(s==null?void 0:s.recent)??[];return e.jsxs(e.Fragment,{children:[e.jsx(_,{message:n}),e.jsxs(M,{title:"Webhook activity",intro:"Sonarr and Radarr are what put files on disk, so they are what the catalogue learns from. A notification is recorded the moment it arrives and read into the catalogue once the file has settled — which is why an import appears here before it appears in Emby.",icon:"plug",tone:"info",children:[e.jsx(ae,{tiles:[{label:"Sonarr webhook",value:s!=null&&s.sonarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.sonarrConfigured?"ok":"warn",icon:"tv"},{label:"Radarr webhook",value:s!=null&&s.radarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.radarrConfigured?"ok":"warn",icon:"play"},{label:"Waiting",value:w((s==null?void 0:s.counts.pending)??0),icon:"clock",tone:"note"},{label:"Given up on",value:w((s==null?void 0:s.counts.failed)??0),icon:"alert",tone:"bad"},{label:"Settle delay",value:`${(s==null?void 0:s.settleSeconds)??0}s`,small:!0,icon:"history",tone:"data"}]}),i?null:e.jsxs("p",{className:"muted",children:["Neither hook has a token, so both answer 404 and nothing is recorded here. Set MEMBY_SONARR_WEBHOOK_TOKEN and MEMBY_RADARR_WEBHOOK_TOKEN, then point each *arr at"," ",e.jsx("code",{children:"/hooks/sonarr"})," and ",e.jsx("code",{children:"/hooks/radarr"}),". Until then the catalogue sweep below is the only way a new title is found."]}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"What"}),e.jsx("th",{children:"Why"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Tries"}),e.jsx("th",{children:"Notes"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(Y,{columns:8,children:i?"Nothing has been imported, upgraded, renamed or deleted since this was switched on.":"No webhook is configured."}):a.map(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(l.updatedAt)}),e.jsx("td",{className:"muted",children:l.source||"—"}),e.jsx("td",{children:Da(l)}),e.jsx("td",{className:"muted",children:l.reason}),e.jsx("td",{children:e.jsx(N,{tone:La[l.state]??"warn",children:l.state})}),e.jsx("td",{className:"muted",children:l.outcome||"—"}),e.jsx("td",{className:"num",children:l.attempts}),e.jsx("td",{className:"muted",children:l.lastError||""})]},l.key))})]})})]})]})}function Fa(){const{status:s,error:n,loading:t}=je(),i=(s==null?void 0:s.runs)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Imports",intro:"What Sonarr and Radarr said changed, and the catalogue sweep that reconciles everything they do not manage."}),e.jsx(_,{message:n}),e.jsx(qa,{}),t?e.jsx(V,{rows:1}):e.jsx(M,{title:"Synchronisation history",intro:"A full import mark-and-sweeps the catalogue; an incremental one asks Emby for what changed, with a minute of overlap so nothing falls between two runs. With both webhooks wired up this is reconciliation — media dropped in by hand, a title edited in Emby, a notification that never arrived — rather than how new titles are found.",icon:"sync",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Started"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"num",children:"Seen"}),e.jsx("th",{className:"num",children:"Written"}),e.jsx("th",{className:"num",children:"Removed"}),e.jsx("th",{children:"Notes"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(Y,{columns:8,children:"Nothing has been imported yet."}):i.map(a=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(a.startedAt)}),e.jsx("td",{children:a.kind}),e.jsx("td",{className:"muted",children:a.trigger}),e.jsx("td",{children:e.jsx(N,{tone:a.status==="success"?"ok":a.status==="running"?"warn":"bad",children:a.status})}),e.jsx("td",{className:"num",children:w(a.itemsSeen)}),e.jsx("td",{className:"num",children:w(a.itemsUpserted)}),e.jsx("td",{className:"num",children:w(a.itemsRemoved)}),e.jsx("td",{className:"muted",children:a.error||""})]},a.id||a.startedAt))})]})})})]})}const Oa={gateway:"quiet",auth:"note",playback:"info",media:"info",emby:"data",library:"data",subtitles:"data",search:"note",tracearr:"idle",credits:"idle",integrations:"note",requests:"info",home:"quiet"},In=s=>Oa[s]??"quiet",Pa={admin:["gateway","Gateway","Admin"],installer:["gateway","Gateway","Installer"],api:["gateway","Gateway","API"],health:["gateway","Gateway","Health"],status:["gateway","Gateway","Status"],maintenance:["gateway","Gateway","Maintenance"],"quiet-time":["gateway","Gateway","Quiet time"],webhooks:["gateway","Gateway","Webhooks"],scheduler:["gateway","Gateway","Scheduler"],settings:["gateway","Gateway","Settings"],updates:["gateway","Gateway","Updates"],analytics:["gateway","Gateway","Analytics"],auth:["auth","Auth","Session"],devices:["auth","Auth","Devices"],playback:["playback","Playback","Session"],screensaver:["media","Media","Screensaver"],artwork:["media","Media","Artwork"],details:["media","Media","Details"],search:["search","Search","Query"],home:["home","Home","Rows"],"my-shows":["home","Home","My shows"],recommendations:["tracearr","Tracearr","Recommendations"],"for-you":["tracearr","Tracearr","For you"],library:["library","Library","Sync"],credits:["credits","Credits","Scanner"],ratings:["media","Media","Ratings"],integrations:["integrations","Integrations","Arr"],requests:["requests","Requests","Media"],"emby-health":["emby","Emby","Health"]},_a=[[/^emby |emby (reachable|unreachable|health)/,["emby","Emby","API"]],[/subtitle/,["subtitles","Subtitles","Provider"]],[/^sonarr|sonarr /,["integrations","Integrations","Sonarr"]],[/^radarr|radarr /,["integrations","Integrations","Radarr"]],[/^tracearr/,["tracearr","Tracearr","Signals"]],[/^credits/,["credits","Credits","Scanner"]],[/^library sync/,["library","Library","Sync"]],[/^(signed in|signed out|sign-in rejected)/,["auth","Auth","Session"]],[/^device /,["auth","Auth","Devices"]],[/^(playback (requested|started|stopped|progress))/,["playback","Playback","Session"]],[/^(next episode resolved|trailer playback|trickplay)/,["playback","Playback","Player"]],[/^scheduled task/,["gateway","Gateway","Scheduler"]],[/^(update offered|update policy)/,["gateway","Gateway","Updates"]]];function Ua(s,n){const t=n.toLowerCase();for(const[a,l]of _a)if(a.test(t))return l;const i=Pa[s];return i||(s?["gateway","Gateway",ms(s.replace(/[-_]/g," "))]:["gateway","Gateway","Server"])}const Wa={h:36e5,m:6e4,s:1e3,ms:1,us:.001,µs:.001,ns:1e-6};function bs(s){if(typeof s=="number")return Number.isFinite(s)?s:null;if(typeof s!="string"||!s)return null;const n=s.matchAll(/([0-9]*\.?[0-9]+)(ns|µs|us|ms|h|m|s)/g);let t=0,i=!1;for(const a of n){const l=a[2]?Wa[a[2]]:void 0;l!==void 0&&(t+=Number(a[1])*l,i=!0)}return i?t:null}function Ts(s){return s<1?"<1 ms":s<1e3?`${Math.round(s)} ms`:s<1e4?`${(s/1e3).toFixed(1)} s`:s<6e4?`${Math.round(s/1e3)} s`:`${Math.floor(s/6e4)}m ${Math.round(s%6e4/1e3)}s`}const Ba=s=>s>=3e3?"bad":s>=1e3?"warn":null,Va={200:"OK",201:"Created",202:"Accepted",204:"No Content",206:"Partial Content",301:"Moved Permanently",302:"Found",304:"Not Modified",400:"Bad Request",401:"Unauthorized",403:"Forbidden",404:"Not Found",405:"Method Not Allowed",409:"Conflict",412:"Precondition Failed",418:"Client Closed Request",426:"Upgrade Required",429:"Too Many Requests",499:"Client Closed Request",500:"Internal Server Error",502:"Bad Gateway",503:"Service Unavailable",504:"Gateway Timeout"};function Ha(s){const n=Va[s];return n||(s>=500?"Server Error":s>=400?"Client Error":s>=300?"Redirected":s>=200?"OK":"Response")}const za=s=>s>=500?"bad":s>=400?"warn":s>=300?"quiet":"ok",Ka={"/healthz":"Health probe","/readyz":"Readiness probe","/v1/status":"Status poll","/v1/home":"Home rows","/v1/features":"Features","/v1/preferences":"Preferences","/v1/theme":"Theme","/v1/magic":"Magic pick","/v1/calendar":"TV calendar","/v1/search":"Search","/v1/update":"Update check"},Ga=s=>/\d/.test(s)||s.length>24;function Xs(s){const n=Ka[s];if(n)return n;const t=s.split("/").filter(a=>a&&a!=="v1"&&a!=="api");t[0]==="admin"&&t.shift();const i=t.filter(a=>!Ga(a));return i.length===0?s:ms(i.join(" ").replace(/[-_.]/g," ").replace(/\s+/g," ").trim())}const ms=s=>s&&s.charAt(0).toUpperCase()+s.slice(1),es=s=>ms(s.replace(/_/g," ")),ve=s=>s==null?"":String(s),Ln=new Set(["","unknown","none","null","","0"]),Me=s=>!Ln.has(ve(s).toLowerCase()),Za=["title","series","name","query","item_title","file"],Ya={directplay:"ok",direct:"ok",directstream:"ok",transcode:"warn",transcoding:"warn"};function Ja(s,n,t){if(t!==null)return{label:`${t} ${Ha(t)}`,short:String(t),tone:za(t)};if(Me(s.error))return{label:"Failed",short:"Failed",tone:n==="WARN"?"warn":"bad"};const i=ve(s.play_method).toLowerCase().replace(/[\s_-]/g,"");if(i&&!Ln.has(i)){const a=ms(ve(s.play_method).replace(/([a-z])([A-Z])/g,"$1 $2"));return{label:a,short:a,tone:Ya[i]??"info"}}if(Me(s.cache)){const a=/hit|true|yes/i.test(ve(s.cache));return{label:a?"Cached":"Cache miss",short:a?"Cached":"Miss",tone:a?"data":"quiet"}}return n==="ERROR"?{label:"Failed",short:"Failed",tone:"bad"}:n==="WARN"?{label:"Warning",short:"Warning",tone:"warn"}:null}function Qa(s){const n=[];Me(s.user)&&n.push(ve(s.user)),Me(s.device)&&n.push(ve(s.device));const t=bs(s.marker_ms);t!==null&&t>0&&n.push(`Start ${en(t)}`);const i=bs(s.position);return i!==null&&i>0&&n.push(`At ${en(i)}`),Me(s.watched)&&n.push(`${ve(s.watched)} watched`),Me(s.reason)&&n.push(ve(s.reason)),n.slice(0,3).join(" · ")}function en(s){const n=Math.round(s/1e3),t=Math.floor(n/3600),i=Math.floor(n%3600/60),a=n%60,l=r=>String(r).padStart(2,"0");return t>0?`${t}:${l(i)}:${l(a)}`:`${i}:${l(a)}`}const Dn=[{title:"Request",keys:["method","path","query_keys","status","cache","client","protocol","host"]},{title:"Context",keys:["user","user_id","device","device_id","item","title","series","type","play_method","play_session_id","media_source_id","position","resume","runtime","watched","subtitles","subtitle_track","subtitle_language","event_name"]},{title:"Where the time went",keys:["duration","breakdown","elsewhere"]},{title:"Diagnostics",keys:["error","stack","correlation","version","gateway_version"]}],Xa=new Set(Dn.flatMap(s=>s.keys)),ei=s=>Xa.has(s),si=new Intl.DateTimeFormat("en-NZ",{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),ni=new Intl.DateTimeFormat("en-NZ",{weekday:"short",day:"numeric",month:"short"}),sn=new WeakMap;function Ze(s){const n=sn.get(s);if(n)return n;const t=ti(s);return sn.set(s,t),t}function ti(s){const n=s.attributes??{},t=s.message??"",[i,a,l]=Ua(ve(n.component),t),r=ve(n.path),d=r?ve(n.method).toUpperCase():"",c=Number(n.status),o=r&&Number.isFinite(c)&&c>0?c:null,h=t==="request"&&!!r,x=Za.find(L=>Me(n[L])),u=x?ve(n[x]):"";let g,b;h?(g=d||"HTTP",b=Xs(r)):r&&d?(g=d,b=u?`${es(t)} · ${u}`:`${es(t)} — ${Xs(r)}`):(g="",b=u?`${es(t)} · ${u}`:es(t));const j=bs(n.duration??n.duration_ms??n.negotiation_duration),m=Me(n.error)?ve(n.error):"",y=Qa(n),f=new Date(s.occurredAt),p=ve(n.breakdown),R=m||(h?p:y),k=m?"error":R?"context":"",$=!m&&h?y:"",I=Object.entries(n);return{serviceKey:i,service:a,component:l,action:g,summary:b,context:y,detail:m,secondary:R,secondaryTone:k,trail:$,result:Ja(n,s.level,o),durationMs:j,method:d,status:o,eventKey:t,level:s.level,time:`${si.format(f)}.${String(f.getMilliseconds()).padStart(3,"0")}`,day:ni.format(f),dayKey:f.toDateString(),tall:!!R,haystack:[t,a,l,b,y,m,...I.flat().map(ve)].join(" ").toLowerCase(),fields:I,attributes:n}}const nn={TRACE:5,DEBUG:10,INFO:20,WARN:30,ERROR:40},ss={level:"INFO",service:"",component:"",event:"",method:"",status:"",slower:0,text:""};function ai(s,n){if(!s)return!0;if(n===null)return!1;if(s==="error")return n>=400;const t=Number(s[0]);return Math.floor(n/100)===t}function ii(s,n,t){if((nn[s.level]??0)<(nn[n.level]??20))return!1;const i=Ze(s);return!(n.service&&i.serviceKey!==n.service||n.component&&i.component!==n.component||n.event&&i.eventKey!==n.event||n.method&&i.method!==n.method||!ai(n.status,i.status)||n.slower>0&&(i.durationMs??0)({key:l,label:r})).sort((l,r)=>l.label.localeCompare(r.label)),components:[...t].sort((l,r)=>l.localeCompare(r)),events:[...i].sort((l,r)=>l.localeCompare(r)),methods:[...a].sort((l,r)=>l.localeCompare(r))}}function li(s,n){var i;const t=[];if(s.service){const a=((i=n.find(l=>l.key===s.service))==null?void 0:i.label)??s.service;t.push({key:"service",label:`Service: ${a}`})}return s.component&&t.push({key:"component",label:`Component: ${s.component}`}),s.event&&t.push({key:"event",label:`Event: ${s.event}`}),s.method&&t.push({key:"method",label:`Method: ${s.method}`}),s.status&&t.push({key:"status",label:`Status: ${s.status==="error"?"≥400":s.status}`}),s.slower>0&&t.push({key:"slower",label:`Duration: >${Ts(s.slower)}`}),s.text&&t.push({key:"text",label:`Search: ${s.text}`}),t}const ns=2e4,oi=5e3,tn=7,ci=16,di=15,hi=2,ui=1,ys=tn+ci+tn+ui,an=ys+hi+di,rn=26,mi=31,ln=10,pi=[{value:"TRACE",label:"Everything"},{value:"DEBUG",label:"Debug+"},{value:"INFO",label:"Info+"},{value:"WARN",label:"Warnings+"},{value:"ERROR",label:"Errors only"}],xi=[{value:"",label:"Any result"},{value:"2xx",label:"Success (2xx)"},{value:"3xx",label:"Redirect (3xx)"},{value:"4xx",label:"Client error (4xx)"},{value:"5xx",label:"Server error (5xx)"},{value:"error",label:"Failed (≥400)"}],ji=[{value:0,label:"Any duration"},{value:100,label:"Slower than 100 ms"},{value:500,label:"Slower than 500 ms"},{value:1e3,label:"Slower than 1 s"},{value:3e3,label:"Slower than 3 s"}],on=s=>s.replace(/_/g," ");function ts({onPick:s,className:n,title:t,children:i,...a}){return e.jsx("button",{type:"button",className:`logfacet ${n}`,title:t,onClick:s,...a,children:i})}const gi=v.memo(function({event:n,view:t,top:i,height:a,selected:l,onInspect:r,onFilter:d}){const c=t.durationMs!==null?Ba(t.durationMs):null;return e.jsxs("div",{className:"logrow","data-level":t.level,"data-selected":l||void 0,style:{transform:`translateY(${i}px)`,height:`${a}px`},children:[e.jsx("time",{className:"logrow-time",title:n.occurredAt,children:t.time}),e.jsx(ts,{className:"logrow-level","data-level":t.level,title:`Show ${t.level} and above`,onPick:()=>d({level:t.level}),children:t.level}),e.jsxs("span",{className:"logrow-place",children:[e.jsx(ts,{className:"logrow-service","data-tone":In(t.serviceKey),title:`Filter to ${t.service}`,onPick:()=>d({service:t.serviceKey,component:""}),children:t.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),e.jsx(ts,{className:"logrow-component",title:`Filter to ${t.component}`,onPick:()=>d({component:t.component}),children:t.component})]}),e.jsxs("button",{type:"button",className:"logrow-summary",title:[t.summary,t.trail,t.secondary].filter(Boolean).join(" — "),onClick:()=>r(n.sequence),children:[e.jsxs("span",{className:"logrow-line",children:[t.action?e.jsx("b",{className:"logrow-action","data-method":t.method||void 0,children:t.action}):null,e.jsx("span",{className:"logrow-text",children:t.summary}),t.trail?e.jsx("span",{className:"logrow-trail",children:t.trail}):null]}),t.secondary?e.jsx("span",{className:"logrow-second","data-tone":t.secondaryTone,children:t.secondaryTone==="error"?`↳ ${t.secondary}`:t.secondary}):null]}),e.jsx("span",{className:"logrow-result",children:t.result?e.jsx(ts,{className:"logrow-verdict","data-tone":t.result.tone,title:t.status!==null?`Filter to ${t.status}`:`Filter to ${t.eventKey}`,onPick:()=>t.status!==null?d({status:`${Math.floor(t.status/100)}xx`}):d({event:t.eventKey}),children:t.result.label}):null}),e.jsx("span",{className:"logrow-duration","data-tone":c??void 0,children:t.durationMs!==null?Ts(t.durationMs):""})]})});function vi({event:s,view:n,onClose:t}){const[i,a]=v.useState(!1),l=n.fields.filter(([c])=>!ei(c)&&c!=="component"),r=async()=>{try{await navigator.clipboard.writeText(JSON.stringify(s,null,2)),a(!0),window.setTimeout(()=>a(!1),1600)}catch{a(!1)}},d=Dn.map(c=>({title:c.title,rows:c.keys.map(o=>[o,n.attributes[o]]).filter(([,o])=>o!=null&&String(o)!=="")})).filter(c=>c.rows.length>0);return e.jsxs("section",{className:"logdrawer","aria-label":`Log record ${s.sequence}`,children:[e.jsxs("header",{className:"logdrawer-head",children:[e.jsxs("div",{children:[e.jsxs("p",{className:"logdrawer-place",children:[e.jsx("span",{className:"logrow-service","data-tone":In(n.serviceKey),children:n.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),n.component]}),e.jsx("b",{children:n.summary}),n.detail?e.jsx("p",{className:"logdrawer-error",children:n.detail}):null]}),e.jsxs("div",{className:"logdrawer-actions",children:[e.jsx(T,{size:"sm",variant:"quiet",onClick:r,icon:"download",children:i?"Copied":"Copy JSON"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:t,icon:"close",children:"Close"})]})]}),e.jsxs("div",{className:"logdrawer-grid",children:[e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Overview"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Time"}),e.jsxs("dd",{children:[n.day," ",n.time]}),e.jsx("dt",{children:"Level"}),e.jsx("dd",{children:n.level}),e.jsx("dt",{children:"Service"}),e.jsxs("dd",{children:[n.service," › ",n.component]}),e.jsx("dt",{children:"Event"}),e.jsx("dd",{children:n.eventKey}),n.result?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Result"}),e.jsx("dd",{children:n.result.label})]}):null,n.durationMs!==null?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Duration"}),e.jsx("dd",{children:Ts(n.durationMs)})]}):null,e.jsx("dt",{children:"Record"}),e.jsxs("dd",{children:["#",s.sequence]})]})]}),d.map(c=>e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:c.title}),e.jsx("dl",{children:c.rows.map(([o,h])=>e.jsxs(v.Fragment,{children:[e.jsx("dt",{children:on(o)}),e.jsx("dd",{children:String(h)})]},o))})]},c.title)),l.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Details"}),e.jsx("dl",{children:l.map(([c,o])=>e.jsxs(v.Fragment,{children:[e.jsx("dt",{children:on(c)}),e.jsx("dd",{children:String(o)})]},c))})]}):null]}),e.jsxs("details",{className:"logdrawer-raw",children:[e.jsx("summary",{children:"Raw event"}),e.jsx("pre",{children:JSON.stringify(s,null,2)})]})]})}function fi(){var He;const[s,n]=v.useState([]),[t,i]=v.useState(0),[a,l]=v.useState(!1),[r,d]=v.useState(0),[c,o]=v.useState(()=>{const C=new URLSearchParams(window.location.search).get("q")??"";return C?{...ss,text:C}:ss}),[h,x]=v.useState(""),[u,g]=v.useState({top:0,height:600}),[b,j]=v.useState(!0),[m,y]=v.useState(null),f=v.useDeferredValue(c.text.trim().toLowerCase()),p=v.useRef(0),R=v.useRef(!1),k=v.useRef(0),$=v.useRef(null),I=v.useRef(!0),A=v.useRef(void 0),L=v.useRef([]),X=v.useRef(a);v.useEffect(()=>{X.current=a},[a]);const le=v.useCallback(C=>{n(B=>{const Q=B.concat(C);return Q.length>ns?Q.slice(Q.length-ns):Q})},[]),ue=v.useCallback(async()=>{if(R.current||document.hidden)return;R.current=!0;const C=k.current,B=[];let Q=0;try{let be=0,Ce;do Ce=await q.get(`/admin/api/events?after=${p.current}&limit=1000`),p.current=Ce.next||p.current,Q+=Ce.dropped||0,B.push(...Ce.events??[]),be+=1;while(Ce.hasMore&&be<20);x("")}catch(be){x(be instanceof Error?be.message:String(be))}finally{B.length>0&&C===k.current&&(X.current?(L.current=L.current.concat(B),L.current.length>ns&&(L.current=L.current.slice(L.current.length-ns)),d(L.current.length)):le(B)),Q>0&&C===k.current&&i(be=>be+Q),R.current=!1}},[le]);v.useEffect(()=>{let C;const B=()=>{window.clearInterval(C),C=document.hidden?void 0:window.setInterval(()=>void ue(),oi)},Q=()=>{B(),document.hidden||ue()};return ue(),B(),document.addEventListener("visibilitychange",Q),()=>{window.clearInterval(C),document.removeEventListener("visibilitychange",Q)}},[ue]);const te=v.useCallback(()=>{X.current=!0,l(!0)},[]),H=v.useCallback(()=>{X.current=!1;const C=L.current;L.current=[],d(0),l(!1),C.length&&le(C)},[le]),re=v.useCallback(C=>{o(B=>({...B,...C}))},[]),S=v.useMemo(()=>s.filter(C=>ii(C,c,f)),[s,c,f]),O=v.useMemo(()=>{const C=new Float64Array(S.length+1),B=new Uint8Array(S.length),Q=new Uint8Array(S.length);let be=0,Ce="";for(let Le=0;Le{let C=0,B=S.length;for(;C>1;(O.tops[Q]??0)+(O.heights[Q]??0)<=z?C=Q+1:B=Q}return Math.max(0,C-ln)},[O,z,S.length]),we=v.useMemo(()=>{const C=z+u.height;let B=ce;for(;B{const C=[];for(let B=ce;Bs.find(C=>C.sequence===m),[s,m]),E=v.useCallback(()=>{const C=$.current;C&&(I.current=!0,C.scrollTop=C.scrollHeight,j(!0),g({top:C.scrollTop,height:C.clientHeight}))},[]);v.useLayoutEffect(()=>{const C=$.current;!C||!I.current||(C.scrollTop=C.scrollHeight,g({top:C.scrollTop,height:C.clientHeight}))},[$e,O.total]),v.useEffect(()=>()=>window.cancelAnimationFrame(A.current??0),[]);const P=()=>{const C=$.current;if(!C)return;const B=C.scrollHeight-C.scrollTop-C.clientHeight{g({top:C.scrollTop,height:C.clientHeight})})},de=()=>{const C=new Blob([JSON.stringify(S,null,2)],{type:"application/json"}),B=document.createElement("a");B.href=URL.createObjectURL(C),B.download=`memby-events-${new Date().toISOString().replace(/[:.]/g,"-")}.json`,B.click(),window.setTimeout(()=>URL.revokeObjectURL(B.href),1e3)},me=v.useMemo(()=>ri(s),[s]),Ie=li(c,me.services);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Server logs",intro:"Structured gateway events as they happen."}),e.jsx(_,{message:h}),e.jsxs(M,{children:[e.jsxs("div",{className:"logbar",children:[e.jsxs("div",{className:"logbar-filters",children:[e.jsxs("select",{"aria-label":"Service",value:c.service,onChange:C=>re({service:C.target.value,component:""}),children:[e.jsx("option",{value:"",children:"All services"}),me.services.map(C=>e.jsx("option",{value:C.key,children:C.label},C.key))]}),e.jsxs("select",{"aria-label":"Component",value:c.component,onChange:C=>re({component:C.target.value}),children:[e.jsx("option",{value:"",children:"All components"}),me.components.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Level",value:c.level,onChange:C=>re({level:C.target.value}),children:pi.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Event",value:c.event,onChange:C=>re({event:C.target.value}),children:[e.jsx("option",{value:"",children:"All events"}),me.events.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Result",value:c.status,onChange:C=>re({status:C.target.value}),children:xi.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Method",value:c.method,onChange:C=>re({method:C.target.value}),children:[e.jsx("option",{value:"",children:"Any method"}),me.methods.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Duration",value:String(c.slower),onChange:C=>re({slower:Number(C.target.value)}),children:ji.map(C=>e.jsx("option",{value:String(C.value),children:C.label},C.value))}),e.jsxs("label",{className:"logsearch",children:[e.jsx(se,{name:"search"}),e.jsx("input",{type:"search",value:c.text,"aria-label":"Search logs",placeholder:"Search person, title, service, component, path, request ID…",onChange:C=>re({text:C.target.value})})]})]}),e.jsxs("div",{className:"logbar-actions",children:[e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>a?H():te(),icon:a?"play":"clock",children:a?r?`Resume (${w(r)})`:"Resume":"Pause"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>{k.current+=1,L.current=[],d(0),n([]),i(0),y(null)},children:"Clear view"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:de,icon:"download",title:"Export the rows matching the current filters, as delivered by the gateway",children:"Export JSON"})]})]}),Ie.length?e.jsxs("div",{className:"logchips",children:[Ie.map(C=>e.jsxs("button",{type:"button",className:"logchip",onClick:()=>re({[C.key]:ss[C.key]}),children:[C.label,e.jsx(se,{name:"close"})]},C.key)),e.jsx("button",{type:"button",className:"logchip logchip-clear",onClick:()=>o(ss),children:"Clear all"})]}):null,e.jsxs("div",{className:"logshell",children:[e.jsxs("div",{className:"logview",ref:$,onScroll:P,role:"log","aria-label":"Server events",children:[e.jsxs("div",{className:"loghead","aria-hidden":"true",children:[e.jsx("span",{children:"Time"}),e.jsx("span",{children:"Level"}),e.jsx("span",{children:"Service"}),e.jsx("span",{children:"Event"}),e.jsx("span",{children:"Result"}),e.jsx("span",{children:"Duration"})]}),S.length===0?e.jsx("p",{className:"empty",children:s.length===0?"Waiting for server events…":"No events match these filters."}):e.jsx("div",{className:"logbody",style:{height:`${O.total}px`},children:Re.map(({event:C,view:B,index:Q})=>e.jsxs(v.Fragment,{children:[O.divider[Q]?e.jsx("div",{className:"logday",style:{transform:`translateY(${(O.tops[Q]??0)-rn}px)`},children:e.jsx("span",{children:B.day})}):null,e.jsx(gi,{event:C,view:B,top:O.tops[Q]??0,height:O.heights[Q]??ys,selected:C.sequence===m,onInspect:y,onFilter:re})]},C.sequence))})]}),!b&&S.length>0?e.jsxs("button",{type:"button",className:"logtail",onClick:E,children:[e.jsx(se,{name:"caret"}),"Jump to latest"]}):null]}),e.jsxs("p",{className:"hint",children:[w(s.length)," retained · ",w(S.length)," matching",S.length?` · ${w(Re.length)} rows mounted`:"",t?` · ${w(t)} overwritten before delivery`:"",a?` · paused${r?`, ${w(r)} held`:""}`:""]}),ge?e.jsx(vi,{event:ge,view:Ze(ge),onClose:()=>y(null)}):s.length?e.jsx(pe,{children:"Select a row to see the full record — request, context, diagnostics and raw event."}):null]})]})}const cn=["home","movies","shows","favorites","search","recent_searches","genre_browse","for_you","for_you_time","recommendation","continue","latest","my_shows","details","playback","magic_movie","notifications","profiles","settings"];function Ee(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function bi(){const[s,n]=v.useState(30),[t,i]=v.useState(""),a=ws(),l=v.useMemo(()=>`/admin/api/journeys${Ve({days:s,userId:t})}`,[s,t]),{data:r,error:d,loading:c}=J(l),o=r==null?void 0:r.stats,h=(r==null?void 0:r.users)??[],x=(r==null?void 0:r.actions)??[],u=(r==null?void 0:r.paths)??[],g=u[0],b=v.useMemo(()=>{const j=new Map(((r==null?void 0:r.features)??[]).map(y=>[y.feature,y])),m=new Map(cn.map((y,f)=>[y,f]));return[...new Set([...cn,...j.keys()])].map(y=>({name:y,stat:j.get(y)})).sort((y,f)=>{var R,k;const p=(((R=f.stat)==null?void 0:R.uses)??0)-(((k=y.stat)==null?void 0:k.uses)??0);return p||(m.get(y.name)??Number.MAX_SAFE_INTEGER)-(m.get(f.name)??Number.MAX_SAFE_INTEGER)})},[r==null?void 0:r.features]);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User journeys",intro:"How viewers move through Memby, use features and complete flows."}),e.jsx(_,{message:d}),e.jsx(M,{title:"Journey health",intro:"Server-derived foreground visits, completion and interruption. Search text, content titles and setting values are never stored.",icon:"people",tone:"info",actions:e.jsxs(e.Fragment,{children:[e.jsx(D,{label:"Window",children:e.jsxs("select",{value:s,onChange:j=>n(Number(j.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"}),e.jsx("option",{value:90,children:"90 days"})]})}),e.jsx(D,{label:"User",children:e.jsxs("select",{value:t,onChange:j=>{const m=j.target.value;i(m),m&&a(`/admin/journeys/${encodeURIComponent(m)}`)},children:[e.jsx("option",{value:"",children:"All users"}),h.map(j=>e.jsx("option",{value:j.userId,children:j.username||j.userId},j.userId))]})})]}),children:c?e.jsx(V,{rows:1}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"journeys",value:w(o==null?void 0:o.journeys),icon:"list",tone:"data"},{label:"viewers",value:w(o==null?void 0:o.viewers),icon:"people",tone:"info"},{label:"completion",value:is(o==null?void 0:o.completionRate),icon:"check",tone:"ok"},{label:"abandoned",value:w(o==null?void 0:o.abandoned),icon:"alert",tone:"note"},{label:"active now",value:w(o==null?void 0:o.active),icon:"pulse",tone:"info"},{label:"average steps",value:((o==null?void 0:o.averageSteps)??0).toFixed(1),icon:"chart"},{label:"average visit",value:ye(o==null?void 0:o.averageTimeMs),small:!0,icon:"clock"},{label:"history kept",value:`${(r==null?void 0:r.retentionDays)??90} days`,small:!0,icon:"clock"}]}),e.jsxs("div",{className:"summary-grid",children:[e.jsxs("div",{className:"summary",children:[e.jsxs("div",{className:"summary-head",children:[e.jsx("b",{children:"Visit completion"}),e.jsx("strong",{children:is(o==null?void 0:o.completionRate)})]}),e.jsx(Tn,{value:(o==null?void 0:o.completed)??0,total:(o==null?void 0:o.journeys)??0}),e.jsxs("p",{children:[w(o==null?void 0:o.completed)," completed · ",w(o==null?void 0:o.abandoned)," abandoned ·"," ",w(o==null?void 0:o.active)," active"]})]}),e.jsxs("div",{className:"summary",children:[e.jsx("div",{className:"summary-head",children:e.jsx("b",{children:"Most common route"})}),e.jsx("strong",{style:void 0,children:g?`${Ee(g.from)} → ${Ee(g.to)}`:"Not enough data"}),e.jsx("p",{children:g?`${w(g.count)} times in this window`:"Journeys will appear here as viewers move through Memby."})]})]})]})}),e.jsxs(xe,{cols:"2",children:[e.jsx(M,{title:"What people do",intro:"Actions show total use and how many separate visits included them.",icon:"chart",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Action"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{className:"num",children:"Visits"})]})}),e.jsx("tbody",{children:x.length===0?e.jsx(Y,{columns:3,children:"No significant actions in this window."}):x.map(j=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:Ee(j.action)}),e.jsx("span",{className:"table-sub",children:Ee(j.category)})]}),e.jsx("td",{className:"num",children:w(j.events)}),e.jsx("td",{className:"num",children:w(j.journeys)})]},`${j.category}:${j.action}`))})]})})}),e.jsx(M,{title:"Where people go",intro:"The most common steps between screens, including where quiet visits ended.",icon:"list",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Route"}),e.jsx("th",{className:"num",children:"Times"})]})}),e.jsx("tbody",{children:u.length===0?e.jsx(Y,{columns:2,children:"No repeated paths in this window."}):u.map((j,m)=>e.jsxs("tr",{children:[e.jsxs("td",{children:[Ee(j.from)," ",e.jsx("span",{className:"route-arrow",children:"→"})," ",Ee(j.to)]}),e.jsx("td",{className:"num",children:w(j.count)})]},`${j.from}:${j.to}:${m}`))})]})})})]}),e.jsx(M,{title:"Feature use",intro:"Rare and unused features are shown against Memby's major feature catalogue.",icon:"pulse",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Feature"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{children:"Last used"}),e.jsx("th",{children:"Status"})]})}),e.jsx("tbody",{children:b.map(({name:j,stat:m})=>{const y=(m==null?void 0:m.uses)??0;return e.jsxs("tr",{children:[e.jsx("td",{children:Ee(j)}),e.jsx("td",{className:"num",children:w(y)}),e.jsx("td",{className:"muted nowrap",children:m?F(m.lastUsedAt):"—"}),e.jsx("td",{children:y===0?e.jsx(N,{tone:"warn",children:"not used"}):y<3?e.jsx(N,{tone:"note",children:"rare"}):e.jsx(N,{tone:"ok",children:"used"})})]},j)})})]})})}),t?null:e.jsx(M,{title:"Inspect a viewer",intro:"Choose a person above to open their dedicated session and viewing-journey timeline.",icon:"journey",tone:"info",children:e.jsx("p",{className:"empty",children:"A viewing journey follows one intent through to playback, so two films watched in a single app session appear as two separate journeys."})})]})}const Te=s=>{const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n},qn=s=>Te((s==null?void 0:s.target)||(s==null?void 0:s.screen)||(s==null?void 0:s.source)||(s==null?void 0:s.feature)),yi=s=>{const n=s.find(t=>t.category==="playback"&&t.action==="request");return n!=null&&n.source?Te(n.source):qn(s[0])},dn=s=>s.itemName?`${Te(s.itemType)} · ${s.itemName}`:s.source&&s.target?`${Te(s.source)} → ${Te(s.target)}`:qn(s),wi=s=>({journey_start:"Opened Memby",home_open:"Opened Memby",journey_end:"Finished session",screen_view:"Viewed",select:"Selected",open:"Opened",close:"Closed",request:s.category==="playback"?"Asked to watch":"Requested",stop:"Left the player",start:s.category==="playback"?s.outcome==="failure"?"Playback failed":"Started watching":"Started",complete:s.category==="playback"?s.outcome==="completed"?"Finished watching":"Stopped watching":"Completed"})[s.action]??Te(s.action);function ki(s){var i;const n=[...s].reverse().find(a=>a.category==="playback"&&a.outcome);if((n==null?void 0:n.outcome)==="failure")return{label:"playback failed",tone:"warn"};if((n==null?void 0:n.outcome)==="completed")return{label:"watched",tone:"ok"};if((n==null?void 0:n.outcome)==="abandoned")return{label:"stopped part-way",tone:"note"};if((n==null?void 0:n.outcome)==="success")return{label:"watched",tone:"ok"};const t=(i=[...s].reverse().find(a=>a.outcome))==null?void 0:i.outcome;return t==="success"||t==="completed"?{label:Te(t),tone:"ok"}:t==="failure"||t==="cancelled"||t==="abandoned"?{label:Te(t),tone:"note"}:s.some(a=>a.action==="stop"&&a.category==="playback")?{label:"watched",tone:"ok"}:{label:"left before playback ended",tone:"warn"}}function Ni(s){const n=s.reduce((t,i,a)=>(i.category==="playback"&&i.action==="request"&&t.push(a),t),[]);return n.length===0?[s]:n.map((t,i)=>s.slice(i===0?0:t,n[i+1]??s.length))}function Si(){var c,o;const{userId:s=""}=Be(),n=v.useMemo(()=>`/admin/api/journeys${Ve({days:90,userId:s})}`,[s]),{data:t,error:i,loading:a}=J(n),l=((o=(c=t==null?void 0:t.users)==null?void 0:c.find(h=>h.userId===s))==null?void 0:o.username)||s,r=v.useMemo(()=>{const h=new Map;for(const x of(t==null?void 0:t.events)??[])h.set(x.journeyId,[...h.get(x.journeyId)??[],x]);return[...h.values()].map(x=>x.sort((u,g)=>u.sequence-g.sequence)).sort((x,u)=>{var g,b;return(((g=u[0])==null?void 0:g.occurredAt)??"").localeCompare(((b=x[0])==null?void 0:b.occurredAt)??"")})},[t==null?void 0:t.events]),d=r.flatMap(h=>Ni(h).map((x,u)=>{var g;return{events:x,key:`${(g=h[0])==null?void 0:g.journeyId}:${u}`}}));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:`${l}'s journeys`,intro:"Each app session is shown as the viewing journeys it contains: entry, selection, playback outcome.",icon:"journey",crumbs:e.jsx(Z,{className:"crumb",to:"/admin/journeys",children:"Journeys"})}),e.jsx(_,{message:i}),a?e.jsx(V,{}):e.jsx(M,{title:"Viewing journeys",intro:`${r.length} app session${r.length===1?"":"s"} · ${d.length} viewing journey${d.length===1?"":"s"} in the last 90 days.`,icon:"journey",tone:"info",children:e.jsx("div",{className:"visits",children:d.length===0?e.jsx("p",{className:"empty",children:"No journeys recorded for this viewer."}):d.map((h,x)=>{const u=h.events,g=u[0],b=[...u].reverse().find(m=>m.itemName||m.action==="select"||m.category==="playback"&&m.action==="request"),j=ki(u);return e.jsxs("article",{className:"visit",children:[e.jsxs("header",{children:[e.jsxs("div",{children:[e.jsx("b",{children:F(g==null?void 0:g.occurredAt)}),e.jsxs("span",{children:["Journey ",x+1," · ",u.length," recorded steps"]})]}),e.jsx(N,{tone:j.tone,children:j.label})]}),e.jsxs("div",{className:"journey-answers",children:[e.jsxs("div",{className:"journey-answer","data-kind":"entry",children:[e.jsx(se,{name:"journey"}),e.jsx("span",{children:"Entered from"}),e.jsx("b",{children:yi(u)})]}),e.jsxs("div",{className:"journey-answer","data-kind":"selection",children:[e.jsx(se,{name:"play"}),e.jsx("span",{children:"Selected"}),e.jsx("b",{children:b?dn(b):"Nothing selected"})]}),e.jsxs("div",{className:"journey-answer","data-kind":"outcome",children:[e.jsx(se,{name:j.tone==="ok"?"check":"clock"}),e.jsx("span",{children:"Outcome"}),e.jsx("b",{children:j.label})]})]}),e.jsx("ol",{className:"journey-timeline",children:u.map(m=>e.jsxs("li",{children:[e.jsx("span",{className:"timeline-dot","data-action":m.action}),e.jsxs("div",{children:[e.jsx("b",{children:wi(m)}),e.jsx("span",{children:dn(m)})]}),e.jsx("time",{children:F(m.occurredAt)})]},`${m.journeyId}:${m.sequence}`))})]},h.key)})})})]})}function hn(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function Ci(){const[s,n]=v.useState(30),{data:t,error:i,loading:a}=J(`/admin/api/analytics?days=${s}`),l=(t==null?void 0:t.rows)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row."}),e.jsx(_,{message:i}),e.jsx(M,{title:"Launcher rows",intro:"Impressions are rows drawn, focuses are rows the D-pad reached, and dwell is how long it stayed there. Open rate is what a row was worth.",icon:"chart",tone:"info",actions:e.jsx(D,{label:"Window",children:e.jsxs("select",{value:s,onChange:r=>n(Number(r.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"}),e.jsx("option",{value:90,children:"90 days"})]})}),children:a?e.jsx(V,{rows:1}):e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Row"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{className:"num",children:"Dwell"}),e.jsx("th",{className:"num",children:"Impressions"}),e.jsx("th",{className:"num",children:"Focuses"}),e.jsx("th",{className:"num",children:"Opened"}),e.jsx("th",{className:"num",children:"Open rate"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:l.length===0?e.jsx(Y,{columns:8,children:"No events in this window."}):l.map(r=>e.jsxs("tr",{children:[e.jsx("td",{children:hn(r.rowId)}),e.jsx("td",{className:"muted",children:hn(r.rowKind)}),e.jsx("td",{className:"num",children:ye(r.dwellMs)}),e.jsx("td",{className:"num",children:w(r.impressions)}),e.jsx("td",{className:"num",children:w(r.focuses)}),e.jsx("td",{className:"num",children:w(r.selects)}),e.jsx("td",{className:"num",children:is(r.selectRate)}),e.jsx("td",{className:"num",children:w(r.viewers)})]},`${r.rowId}:${r.rowKind}`))})]})})})]})}function Mi(){const[s,n]=v.useState(7),{data:t,error:i,loading:a}=J(`/admin/api/searches?days=${s}`),l=(t==null?void 0:t.terms)??[],r=(t==null?void 0:t.recent)??[],d=t==null?void 0:t.totals;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Searches",intro:"What viewers have been looking for, and what was searched just now."}),e.jsx(_,{message:i}),e.jsx(ae,{tiles:[{label:"searches",value:w((d==null?void 0:d.searches)??0),icon:"search",tone:"info"},{label:"distinct queries",value:w((d==null?void 0:d.queries)??0),icon:"list",tone:"data"},{label:"viewers searching",value:w((d==null?void 0:d.viewers)??0),icon:"people",tone:"note"},{label:"history kept",value:`${(t==null?void 0:t.retentionDays)??30} days`,small:!0,icon:"clock"}]}),a?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"What the house looks for",intro:"Queries the search tab ran, grouped without regard to case and labelled with the most recent spelling. Instant search asks from the second character, so a title typed slowly leaves its prefixes here too.",icon:"search",tone:"info",actions:e.jsx(D,{label:"Window",children:e.jsxs("select",{value:s,onChange:c=>n(Number(c.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"})]})}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Query"}),e.jsx("th",{className:"num",children:"Searches"}),e.jsx("th",{className:"num",children:"Viewers"}),e.jsx("th",{children:"Last searched"})]})}),e.jsx("tbody",{children:l.length===0?e.jsx(Y,{columns:4,children:"Nothing searched in this window."}):l.map(c=>e.jsxs("tr",{children:[e.jsx("td",{children:c.query}),e.jsx("td",{className:"num",children:w(c.searches)}),e.jsx("td",{className:"num",children:w(c.viewers)}),e.jsx("td",{className:"muted nowrap",children:F(c.lastAt)})]},c.query))})]})})}),e.jsx(M,{title:"As it happened",intro:"The log, newest first — the query exactly as it was typed, and who typed it. This is the one to read when somebody says search is not finding something.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{children:"Viewer"}),e.jsx("th",{children:"Query"})]})}),e.jsx("tbody",{children:r.length===0?e.jsx(Y,{columns:3,children:"No searches in this window."}):r.map((c,o)=>e.jsxs("tr",{children:[e.jsx("td",{className:"muted nowrap",children:F(c.occurredAt)}),e.jsx("td",{children:c.username||e.jsx(N,{tone:"warn",children:c.userId||"unknown"})}),e.jsx("td",{children:c.query})]},`${c.occurredAt}:${o}`))})]})})})]})]})}function un(s,n){if(n===0)return s>0?"new this week":"no change";const t=Math.round((s-n)/n*100);return`${t>0?"+":""}${t}% vs last week`}function Ti(){const{data:s,error:n,loading:t}=J("/admin/api/views",{pollMs:6e4}),i=(s==null?void 0:s.daily)??[],a=(s==null?void 0:s.hourly)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Views",intro:"How often people reach Memby’s home screen. This measures app use, not playback streams."}),e.jsx(_,{message:n}),t?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:un((s==null?void 0:s.today.visits)??0,(s==null?void 0:s.lastWeek.visits)??0),value:w(s==null?void 0:s.today.visits),icon:"overview",tone:"data"},{label:un((s==null?void 0:s.today.viewers)??0,(s==null?void 0:s.lastWeek.viewers)??0),value:w(s==null?void 0:s.today.viewers),icon:"people",tone:"note"},{label:"busiest time today",value:(s==null?void 0:s.busiestHour)||"—",small:!0,icon:"clock",tone:"info"}]}),e.jsx(M,{title:"Visits by day",intro:"One visit is a signed-in home-screen opening. Viewers are distinct signed-in profiles.",icon:"chart",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Day"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(Y,{columns:3,children:"No home-screen visits yet."}):i.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:w(l.visits)}),e.jsx("td",{className:"num",children:w(l.viewers)})]},l.label))})]})})}),e.jsx(M,{title:"Today by hour",intro:"Local New Zealand time. Use this to see when viewers are opening Memby.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Hour"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(Y,{columns:3,children:"No home-screen visits yet today."}):a.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:w(l.visits)}),e.jsx("td",{className:"num",children:w(l.viewers)})]},l.label))})]})})})]})]})}const Ri=s=>s.mediaType==="episode"?`${s.seriesTitle} S${String(s.seasonNumber).padStart(2,"0")}E${String(s.episodeNumber).padStart(2,"0")}`:s.title;function $i(){var a;const s=J("/admin/api/media-reports",{pollMs:15e3}),{busy:n,run:t}=ne(),i=(l,r)=>t(`${l.id}-${r}`,async()=>{await q.post(`/admin/api/media-reports/${l.id}/status`,{status:r}),await s.reload()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Media reports",intro:"Viewer-reported problems and the individual replacement searches they asked Memby to start."}),s.loading?e.jsx(V,{rows:4}):e.jsx(M,{title:"Open and recent reports",intro:"A replacement always targets one film or one episode. Existing files stay in place while Radarr or Sonarr applies its normal import policy.",icon:"inbox",children:(a=s.data)!=null&&a.reports.length?e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Media"}),e.jsx("th",{children:"Report"}),e.jsx("th",{children:"Viewer"}),e.jsx("th",{children:"Replacement"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Reported"}),e.jsx("th",{children:"Actions"})]})}),e.jsx("tbody",{children:s.data.reports.map(l=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:Ri(l)}),e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.title})]}),e.jsxs("td",{children:[l.reason.replaceAll("_"," "),l.comment?e.jsxs(e.Fragment,{children:[e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.comment})]}):null]}),e.jsxs("td",{children:[l.reportedByUsername,e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.reportedByDevice||"Unknown device"})]}),e.jsx("td",{children:e.jsx(N,{tone:l.replacementRequested?"note":void 0,children:l.replacementRequested?l.replacementStatus||"Requested":"Not requested"})}),e.jsx("td",{children:e.jsx(N,{tone:l.status==="resolved"?"ok":l.status==="dismissed"?void 0:"warn",children:l.status})}),e.jsx("td",{className:"nowrap muted",children:F(l.createdAt)}),e.jsxs("td",{children:[e.jsx(T,{size:"sm",variant:"quiet",busy:n===`${l.id}-acknowledged`,onClick:()=>void i(l,"acknowledged"),children:"Acknowledge"})," ",e.jsx(T,{size:"sm",variant:"quiet",busy:n===`${l.id}-resolved`,onClick:()=>void i(l,"resolved"),children:"Resolve"})," ",e.jsx(T,{size:"sm",variant:"quiet",busy:n===`${l.id}-dismissed`,onClick:()=>void i(l,"dismissed"),children:"Dismiss"})]})]},l.id))})]})}):e.jsx(ee,{children:"No media problems have been reported."})})]})}const mn={user:"",kind:"",channel:"",status:"",source:"",q:"",from:"",to:""},Ei=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:90,label:"90 days"}],Ke=100,Fn={sent:"ok",delivered:"ok",failed:"bad",pending:"warn",skipped:"idle"},Ai={"in-app":"note",broadcast:"info",webhook:"data"},os={"in-app":"In-app",broadcast:"Broadcast",webhook:"Webhook"};function Se(s){if(!s)return"—";const n=s.replace(/[-_.:]+/g," ").trim();return n.charAt(0).toUpperCase()+n.slice(1)}function as(s,n){return(s??[]).map(t=>e.jsxs("option",{value:t.value,children:[n(t.value)," (",t.count,")"]},t.value))}function Ii(){var y,f,p,R;const[s,n]=v.useState(7),[t,i]=v.useState(mn),[a,l]=v.useState(0),[r,d]=v.useState(null),c=v.useMemo(()=>Ve({...t,days:t.from?void 0:s,limit:Ke,offset:a*Ke}),[t,s,a]),o=J(`/admin/api/notification-log${c}`),h=o.data,x=k=>{i($=>({...$,...k})),l(0),d(null)},u=Object.values(t).some(k=>k!==""),g=h==null?void 0:h.totals,b=(h==null?void 0:h.retentionDays)??90,j=(h==null?void 0:h.entries.length)??0,m=!h||h.total===0?0:a*Ke+1;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Notifications",intro:"Everything Memby sent — a viewer's own news, the bar every television draws, and each outbound webhook — with what became of it. Every feature reports through one notification service, so this is the whole trail rather than whichever half a feature remembered to log."}),e.jsx(_,{message:o.error}),g?e.jsx(ae,{tiles:[{label:"Sent",value:w(g.sent),icon:"send",tone:"ok"},{label:"Confirmed",value:w(g.delivered),icon:"check",tone:"ok"},{label:"Failed",value:w(g.failed),icon:"alert",tone:g.failed>0?"bad":void 0},{label:"Skipped",value:w(g.skipped),icon:"filter",tone:"idle"},{label:"People reached",value:w(g.users),icon:"people",tone:"note"},{label:"History kept",value:`${b} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Ue,{value:t.from?-1:s,options:Ei.map(k=>({value:k.value,label:k.label})),onChange:k=>{n(k),x({from:"",to:""})}})}),e.jsx(D,{label:"Person",children:e.jsxs("select",{value:t.user,onChange:k=>x({user:k.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),((h==null?void 0:h.users)??[]).map(k=>e.jsx("option",{value:k.id,children:k.username||k.id},k.id))]})}),e.jsx(D,{label:"Type",children:e.jsxs("select",{value:t.kind,onChange:k=>x({kind:k.target.value}),children:[e.jsx("option",{value:"",children:"Any type"}),as((y=h==null?void 0:h.facets)==null?void 0:y.kinds,Se)]})}),e.jsx(D,{label:"Channel",children:e.jsxs("select",{value:t.channel,onChange:k=>x({channel:k.target.value}),children:[e.jsx("option",{value:"",children:"Any channel"}),as((f=h==null?void 0:h.facets)==null?void 0:f.channels,k=>os[k]??Se(k))]})}),e.jsx(D,{label:"Status",children:e.jsxs("select",{value:t.status,onChange:k=>x({status:k.target.value}),children:[e.jsx("option",{value:"",children:"Any status"}),as((p=h==null?void 0:h.facets)==null?void 0:p.statuses,Se)]})}),e.jsx(D,{label:"Source",children:e.jsxs("select",{value:t.source,onChange:k=>x({source:k.target.value}),children:[e.jsx("option",{value:"",children:"Any service"}),as((R=h==null?void 0:h.facets)==null?void 0:R.sources,Se)]})}),e.jsx(D,{label:"From",children:e.jsx("input",{type:"date",value:t.from,onChange:k=>x({from:k.target.value})})}),e.jsx(D,{label:"To",children:e.jsx("input",{type:"date",value:t.to,onChange:k=>x({to:k.target.value})})}),e.jsx(D,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:t.q,placeholder:"Title, message, error or person",onChange:k=>x({q:k.target.value})})}),e.jsx("div",{className:"filter-actions",children:u?e.jsx(T,{variant:"quiet",size:"sm",onClick:()=>{i(mn),l(0),d(null)},children:"Clear"}):null})]}),o.loading&&!h?e.jsx(V,{}):null,h&&h.days.length>1?e.jsx(M,{title:"Notifications per day",intro:"Failures and deliberate skips are counted beside the deliveries, because a quiet week and a week nothing was allowed to send look identical otherwise.",icon:"chart",tone:"info",children:e.jsx(hs,{data:h.days,labelOf:k=>k.day,valueOf:k=>k.sent+k.delivered+k.failed+k.skipped,toneOf:k=>k.failed>0?"bad":void 0,title:k=>`${k.day}: ${k.sent+k.delivered} sent, ${k.failed} failed, ${k.skipped} skipped`})}):null,h?e.jsx(M,{title:"History",intro:"Newest first. A row says who, what and whether it worked on its own; open one for the whole message and the delivery response.",icon:"send",tone:"ok",actions:e.jsx("span",{className:"filter-summary",children:h.total===0?"nothing matches":`${w(m)}–${w(m+j-1)} of ${w(h.total)}`}),footer:h.total>Ke?e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",disabled:a===0,onClick:()=>l(a-1),children:"Newer"}),e.jsx(T,{size:"sm",disabled:(a+1)*Ke>=h.total,onClick:()=>l(a+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Recipient"}),e.jsx("th",{children:"Type"}),e.jsx("th",{children:"Title"}),e.jsx("th",{children:"Channel"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"Status"}),e.jsx("th",{"aria-label":"Details"})]})}),e.jsx("tbody",{children:h.entries.length===0?e.jsx(Y,{columns:8,children:"No notifications match these filters."}):h.entries.map(k=>e.jsxs(v.Fragment,{children:[e.jsx(Li,{entry:k,open:r===k.id,onToggle:()=>d(r===k.id?null:k.id)}),r===k.id?e.jsx(Di,{entry:k}):null]},k.id))})]})})}):null]})}function Li({entry:s,open:n,onToggle:t}){return e.jsxs("tr",{"data-selected":n||void 0,children:[e.jsx("td",{className:"nowrap muted",children:F(s.occurredAt)}),e.jsx("td",{children:s.userId?e.jsx(Z,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(s.userId)}`,children:s.username||s.userId}):s.target?e.jsx("span",{className:"muted",children:s.target}):e.jsx("span",{className:"quiet",children:"Everyone"})}),e.jsx("td",{className:"mono",children:s.kind||"—"}),e.jsx("td",{children:s.title||e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"nowrap",children:e.jsx(N,{tone:Ai[s.channel],children:os[s.channel]??s.channel})}),e.jsx("td",{className:"muted",children:Se(s.source)}),e.jsx("td",{className:"nowrap",children:e.jsx(N,{tone:Fn[s.status]??"idle",children:s.status})}),e.jsx("td",{className:"nowrap",children:e.jsx(T,{size:"sm",variant:"quiet",onClick:t,icon:n?"close":"list",children:n?"Close":"Details"})})]})}function Di({entry:s}){const n=Object.entries(s.metadata??{}).filter(([,t])=>t!=null&&String(t)!=="");return e.jsx("tr",{className:"detail-row",children:e.jsx("td",{colSpan:8,children:e.jsxs("section",{className:"logdrawer","aria-label":`Notification ${s.id}`,children:[e.jsxs("header",{className:"logdrawer-head",children:[e.jsxs("div",{children:[e.jsxs("p",{className:"logdrawer-place",children:[e.jsx("span",{children:os[s.channel]??s.channel}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),Se(s.source)]}),e.jsx("b",{children:s.title||Se(s.kind)}),s.detail?e.jsx("p",{className:"logdrawer-error",children:s.detail}):null]}),e.jsx("div",{className:"logdrawer-actions",children:e.jsx(N,{tone:Fn[s.status]??"idle",children:s.status})})]}),e.jsxs("div",{className:"logdrawer-grid",children:[e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Delivery"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Sent"}),e.jsx("dd",{children:F(s.occurredAt)}),s.eventAt?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Event"}),e.jsx("dd",{children:F(s.eventAt)})]}):null,e.jsx("dt",{children:"Channel"}),e.jsx("dd",{children:os[s.channel]??s.channel}),e.jsx("dt",{children:"Type"}),e.jsx("dd",{children:s.kind||"—"}),e.jsx("dt",{children:"Service"}),e.jsx("dd",{children:Se(s.source)}),e.jsx("dt",{children:"Took"}),e.jsxs("dd",{children:[s.durationMs,"ms"]})]})]}),e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Recipient"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Person"}),e.jsx("dd",{children:s.userId?s.username||s.userId:"the whole household"}),s.target?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Destination"}),e.jsx("dd",{children:s.target})]}):null,s.itemId?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Title id"}),e.jsx("dd",{children:s.itemId})]}):null,s.sourceKey?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Source key"}),e.jsx("dd",{children:s.sourceKey})]}):null]})]}),n.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Context"}),e.jsx("dl",{children:n.map(([t,i])=>e.jsxs(v.Fragment,{children:[e.jsx("dt",{children:Se(t)}),e.jsx("dd",{children:String(i)})]},t))})]}):null]}),s.body?e.jsxs("div",{className:"logdrawer-raw",children:[e.jsx("h4",{children:"Message"}),e.jsx("p",{className:"notification-body",children:s.body})]}):null]})})})}const qi=s=>s==="detected"?"ok":s==="failed"?"bad":s==="no_match"?"warn":"info",Fi=s=>s==="no_match"?"no match":s,pn=s=>({"live-playback":"Live playback","tracearr-next":"Next episode","tracearr-binge-prefetch":"Binge look-ahead","multi-user-demand":"Multiple viewers"}[s]??s)||"Unknown",xn=(s,n)=>s>0&&n>0?`S${String(s).padStart(2,"0")}E${String(n).padStart(2,"0")}`:"Episode";function Oi(){var j,m,y,f;const s=J("/admin/api/credits?limit=150",{pollMs:15e3}),{wrap:n}=ie(),{busy:t,run:i}=ne(),[a,l]=v.useState(),[r,d]=v.useState(!1);v.useEffect(()=>{!r&&s.data&&l(s.data.settings)},[s.data,r]);const c=p=>{l(R=>R&&{...R,...p}),d(!0)},o=()=>{a&&i("save",async()=>{const p=await n(()=>q.put("/admin/api/credits",a),"Credits scanning settings saved.");p&&(s.set(p),l(p.settings),d(!1))})},h=((j=s.data)==null?void 0:j.history)??[],x=((m=s.data)==null?void 0:m.pending)??[],u=h.filter(p=>p.outcome==="detected").length,g=h.filter(p=>p.outcome==="no_match").length,b=h.filter(p=>p.outcome==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Credits detection",intro:"Control how far ahead Memby scans and see why each episode was selected, what the detector found, and when it may be tried again."}),e.jsx(_,{message:s.error}),s.loading||!a?e.jsx(V,{}):e.jsxs(e.Fragment,{children:[(y=s.data)!=null&&y.enabled?null:e.jsx(pe,{tone:"warn",children:"Credits detection is disabled in the gateway environment. These settings will be retained for the next time it is enabled."}),e.jsx(ae,{tiles:[{label:"Waiting candidates",value:w((f=s.data)==null?void 0:f.queueDepth),icon:"clock",tone:x.length?"info":void 0},{label:"Detected in this history",value:w(u),icon:"check",tone:"ok"},{label:"No match",value:w(g),icon:"search",tone:g?"warn":void 0},{label:"Failed",value:w(b),icon:"alert",tone:b?"bad":void 0}]}),e.jsxs(xe,{cols:"wide",children:[e.jsx(M,{title:"Candidate controls",intro:"The worker remains single-file and scans one episode at a time. These values control what is allowed to wait and how far prediction looks ahead.",icon:"sliders",tone:"note",footer:e.jsx(T,{variant:"primary",icon:"check",busy:t==="save",disabled:!r,onClick:o,children:"Save settings"}),children:e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Candidate limit",hint:"Maximum episodes waiting in the priority queue. Stronger candidates displace weaker ones when it is full.",children:e.jsx("input",{type:"number",min:1,max:100,value:a.candidateLimit,onChange:p=>c({candidateLimit:Number(p.target.value)})})}),e.jsx(D,{label:"Ordinary look-ahead",hint:"Episodes prepared ahead of a normally paced viewer.",children:e.jsx("input",{type:"number",min:1,max:10,value:a.prefetchEpisodes,onChange:p=>c({prefetchEpisodes:Number(p.target.value)})})}),e.jsx(D,{label:"Maximum look-ahead",hint:"Upper bound for fast binge viewing; must not be below the ordinary look-ahead.",children:e.jsx("input",{type:"number",min:a.prefetchEpisodes,max:20,value:a.maxPrefetch,onChange:p=>c({maxPrefetch:Number(p.target.value)})})}),e.jsx(D,{label:"Retry delay (hours)",hint:"After any speculative attempt, keep that episode out of refreshes for this long. Set 0 to allow every refresh.",children:e.jsx("input",{type:"number",min:0,max:720,value:a.retryHours,onChange:p=>c({retryHours:Number(p.target.value)})})})]})}),e.jsxs(M,{title:"How selection works",icon:"sparkle",tone:"data",children:[e.jsx("p",{className:"muted",children:"Recent viewing predicts the next few episodes. Priority favours a programme playing now, then the next episode, fast viewing, and episodes several people are approaching."}),e.jsx("p",{className:"muted",children:"A completed speculative attempt enters the retry delay even when no marker was found. Live playback can still raise an immediate candidate because somebody is waiting for it."})]})]}),e.jsx(M,{title:"Waiting candidates",intro:"The exact worker order after marker checks and retry cooldowns. A refresh may replace this list as viewing changes.",icon:"list",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Episode"}),e.jsx("th",{children:"Reason"}),e.jsx("th",{className:"num",children:"Priority"}),e.jsx("th",{className:"num",children:"Viewers"}),e.jsx("th",{children:"Demand seen"}),e.jsx("th",{children:"Item ID"})]})}),e.jsx("tbody",{children:x.length===0?e.jsx(Y,{columns:6,children:"No episodes are waiting to be scanned."}):x.map(p=>e.jsxs("tr",{children:[e.jsx("td",{children:xn(p.season,p.episode)}),e.jsx("td",{children:e.jsx(N,{tone:"info",children:pn(p.reason)})}),e.jsx("td",{className:"num",children:w(p.priority)}),e.jsx("td",{className:"num",children:w(p.userCount)}),e.jsx("td",{className:"nowrap muted",title:F(p.lastViewed),children:fe(p.lastViewed)}),e.jsx("td",{className:"mono muted",children:p.itemId})]},p.itemId))})]})})}),e.jsx(M,{title:"Scan history",intro:"Completed worker attempts, newest first. Repeated item IDs make an ineffective retry delay visible immediately.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Finished"}),e.jsx("th",{children:"Programme"}),e.jsx("th",{children:"Selected because"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Marker"}),e.jsx("th",{children:"Evidence"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:h.length===0?e.jsx(Y,{columns:7,children:"No credits scans have completed yet."}):h.map(p=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(p.finishedAt),children:fe(p.finishedAt)}),e.jsxs("td",{children:[e.jsx("b",{children:p.seriesName||p.itemName||p.itemId}),e.jsxs("span",{className:"table-sub",children:[xn(p.season,p.episode),p.itemName&&p.seriesName?` · ${p.itemName}`:""]})]}),e.jsxs("td",{children:[e.jsx(N,{tone:"info",children:pn(p.reason)}),e.jsxs("span",{className:"table-sub",children:["priority ",p.priority]})]}),e.jsxs("td",{children:[e.jsx(N,{tone:qi(p.outcome),children:Fi(p.outcome)}),p.error?e.jsx("span",{className:"table-sub",children:p.error}):null]}),e.jsx("td",{className:"nowrap",children:p.markerMs>0?ye(p.markerMs):"—"}),e.jsxs("td",{className:"muted",children:[p.method||"visual",p.confidence>0?` · ${is(p.confidence)}`:"",p.frames>0?` · ${p.frames} frames`:""]}),e.jsx("td",{className:"num muted",children:ye(p.durationMs)})]},p.id))})]})})})]})]})}function Pi(){return e.jsx(U,{title:"No such page",intro:"That address is not part of the console. Use the search in the bar above, or the sections on the left."})}function _i(){return e.jsx(Un,{children:e.jsx(st,{children:e.jsx(rt,{children:e.jsx(ut,{children:e.jsx(Wn,{children:e.jsxs(W,{path:"/admin",element:e.jsx(dt,{}),children:[e.jsx(W,{index:!0,element:e.jsx(pt,{})}),e.jsx(W,{path:"activity",element:e.jsx(jt,{})}),e.jsx(W,{path:"accounts",element:e.jsx(gt,{})}),e.jsx(W,{path:"accounts/:userId",element:e.jsx(vt,{})}),e.jsx(W,{path:"accounts/:userId/settings",element:e.jsx(St,{})}),e.jsx(W,{path:"clients",element:e.jsx(Mt,{})}),e.jsx(W,{path:"logins",element:e.jsx(Rt,{})}),e.jsx(W,{path:"devices/:deviceId",element:e.jsx(It,{})}),e.jsx(W,{path:"library",element:e.jsx(Lt,{})}),e.jsx(W,{path:"requests",element:e.jsx(Dt,{})}),e.jsx(W,{path:"recommendations",element:e.jsx(qt,{})}),e.jsx(W,{path:"inspector",element:e.jsx(Pt,{})}),e.jsx(W,{path:"hero",element:e.jsx(Bt,{})}),e.jsx(W,{path:"metadata-hero",element:e.jsx(Ht,{})}),e.jsx(W,{path:"features",element:e.jsx(zt,{})}),e.jsx(W,{path:"playback",element:e.jsx(Kt,{})}),e.jsx(W,{path:"subtitles",element:e.jsx(Gt,{})}),e.jsx(W,{path:"credits",element:e.jsx(Oi,{})}),e.jsx(W,{path:"updates",element:e.jsx(Zt,{})}),e.jsx(W,{path:"tasks",element:e.jsx(na,{})}),e.jsx(W,{path:"tasks/:taskId",element:e.jsx(la,{})}),e.jsx(W,{path:"integrations",element:e.jsx(ma,{})}),e.jsx(W,{path:"integrations/webhooks",element:e.jsx(va,{})}),e.jsx(W,{path:"integrations/:integrationId",element:e.jsx(Sa,{})}),e.jsx(W,{path:"maintenance",element:e.jsx(Ta,{})}),e.jsx(W,{path:"runtime",element:e.jsx(Ra,{})}),e.jsx(W,{path:"settings",element:e.jsx(Ia,{})}),e.jsx(W,{path:"imports",element:e.jsx(Fa,{})}),e.jsx(W,{path:"logs",element:e.jsx(fi,{})}),e.jsx(W,{path:"journeys",element:e.jsx(bi,{})}),e.jsx(W,{path:"journeys/:userId",element:e.jsx(Si,{})}),e.jsx(W,{path:"views",element:e.jsx(Ti,{})}),e.jsx(W,{path:"engagement",element:e.jsx(Ci,{})}),e.jsx(W,{path:"searches",element:e.jsx(Mi,{})}),e.jsx(W,{path:"media-reports",element:e.jsx($i,{})}),e.jsx(W,{path:"notifications",element:e.jsx(Ii,{})}),e.jsx(W,{path:"overview",element:e.jsx(Es,{to:"/admin",replace:!0})}),e.jsx(W,{path:"ratings",element:e.jsx(Es,{to:"/admin/integrations/mdblist",replace:!0})}),e.jsx(W,{path:"*",element:e.jsx(Pi,{})})]})})})})})})}const On=document.getElementById("root");if(!On)throw new Error("the console has no root element to render into");fn(On).render(e.jsx(v.StrictMode,{children:e.jsx(_i,{})})); diff --git a/admin-ui/dist/index.html b/admin-ui/dist/index.html index 57afc33..e10d98d 100644 --- a/admin-ui/dist/index.html +++ b/admin-ui/dist/index.html @@ -13,7 +13,7 @@ rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Ccircle cx='16' cy='16' r='16' fill='%2352b54b'/%3E%3Ctext x='16' y='23' font-family='system-ui,sans-serif' font-size='19' font-weight='800' text-anchor='middle' fill='%2306240a'%3EM%3C/text%3E%3C/svg%3E" /> - + diff --git a/admin-ui/src/api/types.ts b/admin-ui/src/api/types.ts index 0e021a9..bd6dd4c 100644 --- a/admin-ui/src/api/types.ts +++ b/admin-ui/src/api/types.ts @@ -757,15 +757,16 @@ export interface GoroutineReport { /* ---------- gateway settings ---------- */ -/** GatewaySettings is the operator's overrides. Every field is optional in meaning: an - * empty string or a zero means "whatever the container was started with", and -1 means - * off for the three that can be switched off. */ +/** GatewaySettings is the operator's server document. Environment-backed fields use an + * empty string or zero for "whatever the container was started with"; notification + * display is native to this document and defaults to home only. */ export interface GatewaySettings { timezone: string; logLevel: string; sessionIdleDays: number; sonarrAlertMinutes: number; radarrAlertMinutes: number; + notificationDisplay: string; embyHealthSeconds: number; slowRequestMillis: number; librarySyncMinutes: number; @@ -781,6 +782,7 @@ export interface GatewaySettingValues { sessionIdleDays: number; sonarrAlertMinutes: number; radarrAlertMinutes: number; + notificationDisplay: string; embyHealthSeconds: number; slowRequestMillis: number; librarySyncMinutes: number; @@ -791,6 +793,7 @@ export interface GatewaySettingsResponse { deployed: GatewaySettingValues; effective: GatewaySettingValues; logLevels: string[] | null; + notificationDisplays: string[] | null; version: string; } diff --git a/admin-ui/src/pages/Settings.tsx b/admin-ui/src/pages/Settings.tsx index 76de479..1eb42b0 100644 --- a/admin-ui/src/pages/Settings.tsx +++ b/admin-ui/src/pages/Settings.tsx @@ -13,10 +13,9 @@ import { Banner, Button, Card, Field, KeyValue, Loading, Note, PageHead, Tag } f * has not been switched on. That is why it is reached from the account menu rather than * from the rail: it belongs beside "signed in as", not beside the household's content. * - * The whole page is an amendment to `.env`. Every field is blank by default and blank - * means "whatever this container was started with", which is printed beside it — so an - * operator can always see what they are overriding, and clearing a field is a real undo - * rather than a value they have to remember. */ + * Most fields amend `.env`: blank means "whatever this container was started with", which + * is printed beside the field. Notification display is server-native instead and has a + * Home-only default. */ /** OVERRIDE_OFF is the wire value for "switched off", which the three window settings need * to distinguish from an empty field. See store.GatewaySettingsOff. */ @@ -46,12 +45,21 @@ function describe(value: number, unit: string): string { return `${value} ${unit}${value === 1 ? '' : 's'}`; } +function notificationDisplayLabel(value: string): string { + switch (value) { + case 'everywhere': return 'Everywhere'; + case 'off': return 'Off'; + default: return 'Home only'; + } +} + interface Draft { timezone: string; logLevel: string; sessionIdleDays: string; sonarrAlertMinutes: string; radarrAlertMinutes: string; + notificationDisplay: string; embyHealthSeconds: string; slowRequestMillis: string; librarySyncMinutes: string; @@ -64,6 +72,7 @@ function draftFrom(settings: GatewaySettings): Draft { sessionIdleDays: numberFieldValue(settings.sessionIdleDays), sonarrAlertMinutes: numberFieldValue(settings.sonarrAlertMinutes), radarrAlertMinutes: numberFieldValue(settings.radarrAlertMinutes), + notificationDisplay: settings.notificationDisplay || 'home_only', embyHealthSeconds: numberFieldValue(settings.embyHealthSeconds), slowRequestMillis: numberFieldValue(settings.slowRequestMillis), librarySyncMinutes: numberFieldValue(settings.librarySyncMinutes), @@ -95,6 +104,7 @@ export function SettingsPage() { sessionIdleDays: parseNumberField(draft.sessionIdleDays, false), sonarrAlertMinutes: parseNumberField(draft.sonarrAlertMinutes, true), radarrAlertMinutes: parseNumberField(draft.radarrAlertMinutes, true), + notificationDisplay: draft.notificationDisplay, embyHealthSeconds: parseNumberField(draft.embyHealthSeconds, true), slowRequestMillis: parseNumberField(draft.slowRequestMillis, true), librarySyncMinutes: parseNumberField(draft.librarySyncMinutes, true), @@ -117,9 +127,9 @@ export function SettingsPage() { () => api.post('/admin/api/gateway-settings', { timezone: '', logLevel: '', sessionIdleDays: 0, sonarrAlertMinutes: 0, radarrAlertMinutes: 0, embyHealthSeconds: 0, slowRequestMillis: 0, - librarySyncMinutes: 0, + librarySyncMinutes: 0, notificationDisplay: 'home_only', }), - 'Every setting is back to what this container was deployed with.', + 'Every setting is back to its deployed or server default.', ); if (saved) setDraft(draftFrom(saved.settings)); await reload(); @@ -128,6 +138,7 @@ export function SettingsPage() { const deployed = data?.deployed; const effective = data?.effective; const levels = data?.logLevels ?? []; + const notificationDisplays = data?.notificationDisplays ?? ['everywhere', 'home_only', 'off']; return ( <> @@ -158,13 +169,14 @@ export function SettingsPage() { { label: 'Catalogue sweep', value: describe(effective.librarySyncMinutes, 'minute') }, { label: 'Episode alert window', value: describe(effective.sonarrAlertMinutes, 'minute') }, { label: 'Film alert window', value: describe(effective.radarrAlertMinutes, 'minute') }, + { label: 'Notification display', value: notificationDisplayLabel(effective.notificationDisplay) }, ]} /> } @@ -262,6 +274,22 @@ export function SettingsPage() { +
+ + + +
+
- These override the deployed configuration in the database, so they survive a - restart — but a deployment rewrites .env, not this, and the two can - then disagree. Anything meant to be permanent belongs in .env.example{' '} - as well. + These settings live in the database and survive a restart. A deployment rewrites + .env, not this document, so environment-backed values can then disagree; + permanent environment changes belong in .env.example as well. {data?.settings.updatedBy ? ( diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e45e059..cc8f50d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -62,7 +62,7 @@ val projectNoticeText = rootProject.file("NOTICE").readText() .replace("https://g.sublogue.com/admin/memby", membySourceUrl) -val defaultVersionName = "0.3.04" +val defaultVersionName = "0.3.05" val membyVersionName: String = (project.findProperty("memby.versionName") as String?) ?.trim() diff --git a/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt b/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt index 45c8aa3..8bff6b7 100644 --- a/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt +++ b/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt @@ -39,7 +39,7 @@ data class EmbyOutage( /** * One informational banner: a show aired, a film was added, the library finished * refreshing, the server stopped answering. It is never actionable and never focusable — - * it slides in, says its piece and goes, over the launcher or over playback alike. + * it slides in, says its piece and goes wherever the server's display policy permits. * * [label] is the eyebrow above the title and comes from the server, so a kind of news * this build has never heard of still reads correctly; a server that sends none gets the @@ -83,6 +83,9 @@ class MaintenanceMonitor( private val _alert = MutableStateFlow(null) val alert: StateFlow = _alert.asStateFlow() + private val _notificationDisplay = MutableStateFlow(NotificationDisplayPolicy.HOME_ONLY) + val notificationDisplay: StateFlow = _notificationDisplay.asStateFlow() + private val _embyOutage = MutableStateFlow(null) /** Null whenever Emby is answering, or when nothing is watching it. */ @@ -296,6 +299,7 @@ class MaintenanceMonitor( _viewersEnabled.value = false setRequestsAllowed(false) _gatewayVersion.value = "" + _notificationDisplay.value = NotificationDisplayPolicy.HOME_ONLY dismissAlert() return@collectLatest } @@ -344,6 +348,8 @@ class MaintenanceMonitor( _viewersEnabled.value = status.features[VIEWERS_FEATURE] == true setRequestsAllowed(status.requests.allowed) _gatewayVersion.value = status.gatewayVersion + _notificationDisplay.value = + NotificationDisplayPolicy.fromWire(status.notificationDisplay) // Emby's state is reported even during maintenance: an // operator taking Memby down while Emby is also unreachable // should not have that fact disappear from the poll. @@ -387,6 +393,7 @@ class MaintenanceMonitor( _viewersEnabled.value = false setRequestsAllowed(false) _gatewayVersion.value = "" + _notificationDisplay.value = NotificationDisplayPolicy.HOME_ONLY dismissAlert() return@collectLatest } diff --git a/app/src/main/java/com/ponzischeme89/memby/data/NotificationDisplayPolicy.kt b/app/src/main/java/com/ponzischeme89/memby/data/NotificationDisplayPolicy.kt new file mode 100644 index 0000000..cd92636 --- /dev/null +++ b/app/src/main/java/com/ponzischeme89/memby/data/NotificationDisplayPolicy.kt @@ -0,0 +1,37 @@ +package com.ponzischeme89.memby.data + +/** Household-wide notification placement chosen by the server administrator. */ +enum class NotificationDisplayPolicy(val wireValue: String) { + EVERYWHERE("everywhere"), + HOME_ONLY("home_only"), + OFF("off"), + ; + + companion object { + /** Missing and unknown values fail safe: browsing is allowed, playback is not. */ + fun fromWire(value: String?): NotificationDisplayPolicy = + entries.firstOrNull { it.wireValue == value?.trim()?.lowercase() } ?: HOME_ONLY + } +} + +/** The two display contexts that matter to the global interruption policy. */ +enum class NotificationSurface { + BROWSING, + PLAYBACK, +} + +/** + * The single eligibility rule for server notifications. + * + * Callers identify their surface; this function owns what each server policy means. New + * notification renderers should use this rather than interpreting the wire value or + * making their own playback decision. + */ +fun notificationEligible( + policy: NotificationDisplayPolicy, + surface: NotificationSurface, +): Boolean = when (policy) { + NotificationDisplayPolicy.EVERYWHERE -> true + NotificationDisplayPolicy.HOME_ONLY -> surface == NotificationSurface.BROWSING + NotificationDisplayPolicy.OFF -> false +} diff --git a/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt b/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt index 92d7382..de66cd5 100644 --- a/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt +++ b/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt @@ -213,6 +213,8 @@ data class GatewayServiceStatus( val maintenance: Boolean = false, val message: String = "", val alerts: List = emptyList(), + /** Server-owned placement policy. Missing or unreadable means home only. */ + val notificationDisplay: String = "home_only", val compatible: Boolean = true, val compatibilityMessage: String = "", val clientVersion: String = "", diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/HomeMovieHero.kt b/app/src/main/java/com/ponzischeme89/memby/ui/HomeMovieHero.kt index b84774a..3338ee9 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/HomeMovieHero.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/HomeMovieHero.kt @@ -2,10 +2,10 @@ package com.ponzischeme89.memby.ui import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -28,12 +28,17 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CornerRadius import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Offset +import androidx.compose.ui.graphics.Size +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight @@ -62,6 +67,8 @@ import kotlinx.coroutines.delay internal const val HOME_HERO_ROW_ID = "home-movie-hero" +private val HeroFocusRingWidth = 2.dp + /** * The current local day, re-read when the clock passes midnight. * @@ -385,17 +392,11 @@ private fun FeaturedMovieCard( // Not "Featured movie": a series premiere can lead, and a screen reader announcing // one as a movie is worse than one announcing it by the caption it is wearing. contentDescription = "Featured, ${pick.label}, ${item.name}", - modifier = modifier.clip(RoundedCornerShape(MembyPanelCorner)), + modifier = modifier, ) { focused -> - Box( - Modifier - .fillMaxSize() - .background(MembySurfaceRaised) - .border( - 2.dp, - if (focused) Color.White else Color.Transparent, - RoundedCornerShape(MembyPanelCorner), - ), + HeroFocusFrame( + focused = focused, + modifier = Modifier.fillMaxSize(), ) { HeroArtwork(item, previewArtwork, Modifier.fillMaxSize()) Box( @@ -547,17 +548,11 @@ private fun MiniMovieCard( onFocused = onFocused, onClick = onClick, contentDescription = "${pick.label} movie, ${item.name}", - modifier = modifier.clip(RoundedCornerShape(MembyPanelCorner)), + modifier = modifier, ) { focused -> - Box( - Modifier - .fillMaxSize() - .background(MembySurfaceRaised) - .border( - 2.dp, - if (focused) Color.White else Color.Transparent, - RoundedCornerShape(MembyPanelCorner), - ), + HeroFocusFrame( + focused = focused, + modifier = Modifier.fillMaxSize(), ) { HeroArtwork(item, previewArtwork, Modifier.fillMaxSize()) Box(Modifier.fillMaxSize().background(labelTint(pick.label))) @@ -604,6 +599,47 @@ private fun MiniMovieCard( } } +/** + * The common artwork boundary for both hero sizes. + * + * The artwork is clipped to the hero shape, while the focus stroke is drawn immediately + * outside that boundary. Keeping those as separate modifier layers means the ring neither + * clips itself nor consumes any of the artwork, and its physical width stays the same for + * the featured card and every mini card. + */ +@Composable +private fun HeroFocusFrame( + focused: Boolean, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val shape = RoundedCornerShape(MembyPanelCorner) + Box( + modifier = modifier + .drawWithCache { + val strokeWidth = HeroFocusRingWidth.toPx() + val halfStroke = strokeWidth / 2f + val ringBounds = Size(size.width + strokeWidth, size.height + strokeWidth) + val ringCorner = CornerRadius(MembyPanelCorner.toPx() + halfStroke) + onDrawWithContent { + drawContent() + if (focused) { + drawRoundRect( + color = Color.White, + topLeft = Offset(-halfStroke, -halfStroke), + size = ringBounds, + cornerRadius = ringCorner, + style = Stroke(strokeWidth), + ) + } + } + } + .clip(shape) + .background(MembySurfaceRaised), + content = content, + ) +} + @Composable private fun HeroArtwork(item: BaseItem, previewArtwork: ImageBitmap?, modifier: Modifier) { if (previewArtwork != null) { diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt b/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt index 47ea982..1869e57 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt @@ -68,6 +68,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.ponzischeme89.memby.ServiceLocator import com.ponzischeme89.memby.data.Settings +import com.ponzischeme89.memby.data.NotificationSurface import com.ponzischeme89.memby.data.StartupPosterSnapshotCache import com.ponzischeme89.memby.data.StartupPosterSource import com.ponzischeme89.memby.data.friendlyEmbyError @@ -2765,6 +2766,7 @@ internal fun HomeScreen( // News about the library, not about the app: it sits above the rows but is // suppressed whenever something more important owns the screen. ServiceAlertBanner( + surface = NotificationSurface.BROWSING, // The alert itself is collected inside the banner, so an arriving one does // not recompose this whole function. Only the suppression conditions — both // already read here for other reasons — cross the boundary. diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/ServiceAlertBanner.kt b/app/src/main/java/com/ponzischeme89/memby/ui/ServiceAlertBanner.kt index 896c894..ed9eab4 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/ServiceAlertBanner.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/ServiceAlertBanner.kt @@ -50,7 +50,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.ponzischeme89.memby.R import com.ponzischeme89.memby.ServiceLocator import com.ponzischeme89.memby.data.MaintenanceMonitor +import com.ponzischeme89.memby.data.NotificationSurface import com.ponzischeme89.memby.data.ServiceAlert +import com.ponzischeme89.memby.data.notificationEligible import kotlin.math.ceil import com.ponzischeme89.memby.ui.theme.MembyAccent @@ -61,8 +63,8 @@ private val AlertBody = Color(0xFFC3CBD2) /** * Height of the bar itself, before the rule and fade that blend it into the screen. * - * Kept to a strip rather than a panel because this now appears over playback as well as - * over the launcher: whatever it says, it is covering somebody's film while it says it. + * Kept to a strip rather than a panel because the server may permit it over playback as + * well as over the launcher. */ private val BannerHeight = 48.dp @@ -83,12 +85,19 @@ private val SafeAreaHorizontal = 48.dp * allowed to own the whole display. */ @Composable -fun ServiceAlertBanner(suppressed: Boolean, modifier: Modifier = Modifier) { +fun ServiceAlertBanner( + surface: NotificationSurface, + suppressed: Boolean, + modifier: Modifier = Modifier, +) { // The alert is collected *here* rather than passed down from the home composable. // Read one scope up and every arriving alert would recompose the entire launcher // body; read here and it recomposes a bar that is usually not even on screen. val current by ServiceLocator.maintenance.alert.collectAsStateWithLifecycle() - val alert = current?.takeUnless { suppressed } + val policy by ServiceLocator.maintenance.notificationDisplay.collectAsStateWithLifecycle() + val alert = current?.takeIf { + !suppressed && notificationEligible(policy, surface) + } // Tell the monitor the moment this is really on screen. Until it hears that, the // alert is only offered — it starts no dismissal timer and persists nothing — so an diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt index 7642480..c8a1463 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/LandscapeCard.kt @@ -131,6 +131,7 @@ fun LandscapeCard( modifier: Modifier = Modifier, density: String = "standard", showWatchedEpisodeCount: Boolean = false, + showFavouriteIndicator: Boolean = true, ) { val cardsAcross = when (density) { "compact" -> 5 @@ -138,12 +139,13 @@ fun LandscapeCard( else -> 4 } val width = responsiveRowCardWidth(availableWidth, cardsAcross, 164.dp, 360.dp) - val model = remember(item, showSecondaryMetadata, showWatchedEpisodeCount) { + val model = remember(item, showSecondaryMetadata, showWatchedEpisodeCount, showFavouriteIndicator) { item.toMediaCardUiModel( showProgress = false, showSecondaryMetadata, showWatchedEpisodeCount, showMediaTypeIcon = false, + showFavouriteIndicator = showFavouriteIndicator, ) } MediaCard( diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt index 0c4a9dd..4a04c06 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/components/media/PosterCard.kt @@ -107,12 +107,15 @@ internal data class MediaCardUiModel( /** * @param showProgress whether a "Resume at …" subtitle should replace the default one when * the item has a saved position — a row-level choice, not a fact about the item. + * @param showFavouriteIndicator whether favourite state adds a heart to the card. Favourites + * rows turn this off because the surrounding row already communicates that state. */ internal fun BaseItem.toMediaCardUiModel( showProgress: Boolean, showSecondaryMetadata: Boolean, showWatchedEpisodeCount: Boolean, showMediaTypeIcon: Boolean, + showFavouriteIndicator: Boolean, ): MediaCardUiModel { val position = userData?.playbackPositionTicks ?: 0L val runtime = runTimeTicks ?: 0L @@ -128,7 +131,7 @@ internal fun BaseItem.toMediaCardUiModel( showSecondaryMetadata = showSecondaryMetadata, contentDescription = cardDescription(this, progress, showWatchedEpisodeCount, showMediaTypeIcon), played = userData?.played == true, - favourite = isFavorite, + favourite = isFavorite && showFavouriteIndicator, progress = progress, isSchedule = isSchedule, scheduleAvailability = membyAvailability.orEmpty(), @@ -152,6 +155,7 @@ fun PosterCard( density: String = "standard", showWatchedEpisodeCount: Boolean = false, showProgress: Boolean = false, + showFavouriteIndicator: Boolean = true, ) { val cardsAcross = when (density) { "compact" -> 8 @@ -159,8 +163,20 @@ fun PosterCard( else -> 7 } val width = responsiveRowCardWidth(availableWidth, cardsAcross, 102.dp, 218.dp) - val model = remember(item, showProgress, showSecondaryMetadata, showWatchedEpisodeCount) { - item.toMediaCardUiModel(showProgress, showSecondaryMetadata, showWatchedEpisodeCount, showMediaTypeIcon = false) + val model = remember( + item, + showProgress, + showSecondaryMetadata, + showWatchedEpisodeCount, + showFavouriteIndicator, + ) { + item.toMediaCardUiModel( + showProgress, + showSecondaryMetadata, + showWatchedEpisodeCount, + showMediaTypeIcon = false, + showFavouriteIndicator = showFavouriteIndicator, + ) } MediaCard( model, item, width, 2f / 3f, preferPrimary = true, showProgress, @@ -192,6 +208,7 @@ fun PosterGridCard( showSecondaryMetadata = true, showWatchedEpisodeCount, showMediaTypeIcon, + showFavouriteIndicator = true, ) } MediaCard( diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt index 8d06cb1..3991307 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/home/HomeRow.kt @@ -319,12 +319,14 @@ internal fun MediaRow( item, availableWidth, row.showSecondaryMetadata, focused, { onItemSelected(item) }, { onItemLongPressed(item) }, cardModifier, density, row.showWatchedEpisodeCount, + showFavouriteIndicator = row.kind != MediaRowKind.FAVORITES, ) MediaCardFormat.LANDSCAPE -> { LandscapeCard( item, availableWidth, row.showSecondaryMetadata, focused, { onItemSelected(item) }, { onItemLongPressed(item) }, cardModifier, density, row.showWatchedEpisodeCount, + showFavouriteIndicator = row.kind != MediaRowKind.FAVORITES, ) } } diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt b/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt index 9f004e7..40f659b 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt @@ -69,6 +69,7 @@ import com.ponzischeme89.memby.data.analytics.PlaybackEntryPoint import com.ponzischeme89.memby.data.analytics.PlaybackJourney import com.ponzischeme89.memby.data.creditsWorthShowing import com.ponzischeme89.memby.data.NextEpisode +import com.ponzischeme89.memby.data.NotificationSurface import com.ponzischeme89.memby.data.ResolvedRemoteTrailer import com.ponzischeme89.memby.data.Playable import com.ponzischeme89.memby.data.PlayableSubtitle @@ -4647,12 +4648,12 @@ class PlayerActivity : ComponentActivity() { } /** - * Hosts the launcher's service-alert bar over the video. + * Hosts the launcher's service-alert bar over the video when the server permits it. * - * News about the server is worth more here than anywhere else — playback direct-plays - * from Emby, so "the server has stopped communicating" explains a stall the viewer is - * looking at right now. The bar is never focusable, so the transport controls keep the - * remote; it times itself out rather than asking for a press. + * Under Everywhere, news about the server can explain a stall the viewer is looking at + * right now. The bar is never focusable, so the transport controls keep the remote; it + * times itself out rather than asking for a press. Home only and Off remain eligible + * nowhere in this activity through the shared notification rule. * * [alertsSuppressed] is what stops an alert being *used up* behind the loading or * error overlay: the banner records an alert as seen the moment it composes, and the @@ -4675,6 +4676,7 @@ class PlayerActivity : ComponentActivity() { .collectAsStateWithLifecycle() EmbyOutageBanner(suppressed = alertsSuppressed.value) ServiceAlertBanner( + surface = NotificationSurface.PLAYBACK, suppressed = alertsSuppressed.value || outage != null, ) } diff --git a/app/src/test/java/com/ponzischeme89/memby/data/ServiceAlertTest.kt b/app/src/test/java/com/ponzischeme89/memby/data/ServiceAlertTest.kt index c4be74e..bede294 100644 --- a/app/src/test/java/com/ponzischeme89/memby/data/ServiceAlertTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/data/ServiceAlertTest.kt @@ -15,6 +15,30 @@ class ServiceAlertTest { private fun alert(id: String, title: String = "Northbound", message: String = "S02E04 aired") = GatewayAlert(id = id, kind = "sonarr-aired", title = title, message = message) + @Test + fun `home only allows browsing notifications but not playback overlays`() { + assertTrue(notificationEligible(NotificationDisplayPolicy.HOME_ONLY, NotificationSurface.BROWSING)) + assertFalse(notificationEligible(NotificationDisplayPolicy.HOME_ONLY, NotificationSurface.PLAYBACK)) + } + + @Test + fun `everywhere allows both notification surfaces`() { + assertTrue(notificationEligible(NotificationDisplayPolicy.EVERYWHERE, NotificationSurface.BROWSING)) + assertTrue(notificationEligible(NotificationDisplayPolicy.EVERYWHERE, NotificationSurface.PLAYBACK)) + } + + @Test + fun `off prevents notifications on every surface`() { + assertFalse(notificationEligible(NotificationDisplayPolicy.OFF, NotificationSurface.BROWSING)) + assertFalse(notificationEligible(NotificationDisplayPolicy.OFF, NotificationSurface.PLAYBACK)) + } + + @Test + fun `missing and unknown server policy fail safe to home only`() { + assertEquals(NotificationDisplayPolicy.HOME_ONLY, NotificationDisplayPolicy.fromWire(null)) + assertEquals(NotificationDisplayPolicy.HOME_ONLY, NotificationDisplayPolicy.fromWire("unexpected")) + } + @Test fun `picks the first alert this tv has not seen`() { val chosen = firstUnseenAlert( @@ -68,6 +92,7 @@ class ServiceAlertTest { """{"maintenance":false,"message":""}""", ) assertTrue(status.alerts.isEmpty()) + assertEquals("home_only", status.notificationDisplay) } @Test @@ -89,6 +114,18 @@ class ServiceAlertTest { assertEquals("", alert.label) } + @Test + fun `status decodes the server notification display policy`() { + val status = json.decodeFromString( + """{"maintenance":false,"notificationDisplay":"everywhere"}""", + ) + + assertEquals( + NotificationDisplayPolicy.EVERYWHERE, + NotificationDisplayPolicy.fromWire(status.notificationDisplay), + ) + } + @Test fun `status decodes a radarr import alert with its own wording`() { val status = json.decodeFromString( diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt index c176612..d2d1e1e 100644 --- a/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt +++ b/app/src/test/java/com/ponzischeme89/memby/ui/HomeMovieHeroScreenshotTest.kt @@ -22,11 +22,17 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.input.key.Key import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.test.assertIsFocused import androidx.compose.ui.test.junit4.v2.createComposeRule import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performKeyInput +import androidx.compose.ui.test.pressKey +import androidx.compose.ui.test.requestFocus import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex @@ -48,6 +54,7 @@ import org.robolectric.annotation.GraphicsMode @RunWith(RobolectricTestRunner::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Config(sdk = [34], qualifiers = "w960dp-h540dp-television-xhdpi") +@OptIn(androidx.compose.ui.test.ExperimentalTestApi::class) class HomeMovieHeroScreenshotTest { @get:Rule val compose = createComposeRule() @@ -62,6 +69,25 @@ class HomeMovieHeroScreenshotTest { capture("df_home-movie-hero", movies) } + @Test + fun `hero focus treatment matches featured and mini cards`() { + setHeroContent(movies) + + compose.onNodeWithContentDescription("Featured, NEW RELEASE, The Last Horizon") + .requestFocus() + .assertIsFocused() + compose.onRoot().captureRoboImage( + "build/screenshots/home-movie-hero/featured-focused.png", + ) + + compose.onRoot().performKeyInput { pressKey(Key.DirectionRight) } + compose.onNodeWithContentDescription("POPULAR movie, Midnight Signal") + .assertIsFocused() + compose.onRoot().captureRoboImage( + "build/screenshots/home-movie-hero/mini-focused.png", + ) + } + /** * The case the audit reproduced: with a title long enough to wrap, the card's content * column used to overflow its fixed height around the individual Play chip. The hero @@ -287,6 +313,11 @@ class HomeMovieHeroScreenshotTest { } private fun capture(name: String, movies: List) { + setHeroContent(movies) + compose.onRoot().captureRoboImage("build/screenshots/home-movie-hero/$name.png") + } + + private fun setHeroContent(movies: List) { val artwork = requireNotNull(javaClass.getResourceAsStream("/home_hero_preview_art.png")) .use(BitmapFactory::decodeStream) .asImageBitmap() @@ -364,7 +395,6 @@ class HomeMovieHeroScreenshotTest { } assertTrue(compose.onAllNodesWithText("Play").fetchSemanticsNodes().isEmpty()) - compose.onRoot().captureRoboImage("build/screenshots/home-movie-hero/$name.png") } private val movies = listOf( diff --git a/app/src/test/java/com/ponzischeme89/memby/ui/MediaCardUiModelTest.kt b/app/src/test/java/com/ponzischeme89/memby/ui/MediaCardUiModelTest.kt new file mode 100644 index 0000000..b20bdfc --- /dev/null +++ b/app/src/test/java/com/ponzischeme89/memby/ui/MediaCardUiModelTest.kt @@ -0,0 +1,33 @@ +package com.ponzischeme89.memby.ui + +import com.ponzischeme89.memby.data.model.BaseItem +import com.ponzischeme89.memby.data.model.UserItemData +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class MediaCardUiModelTest { + private val favourite = BaseItem( + id = "favourite", + userData = UserItemData(isFavorite = true), + ) + + @Test + fun `favourite indicator remains visible on ordinary browsing cards`() { + assertTrue(favourite.cardModel(showFavouriteIndicator = true).favourite) + } + + @Test + fun `favourite indicator can be hidden when the browsing context is favourites`() { + assertFalse(favourite.cardModel(showFavouriteIndicator = false).favourite) + } + + private fun BaseItem.cardModel(showFavouriteIndicator: Boolean): MediaCardUiModel = + toMediaCardUiModel( + showProgress = false, + showSecondaryMetadata = true, + showWatchedEpisodeCount = false, + showMediaTypeIcon = false, + showFavouriteIndicator = showFavouriteIndicator, + ) +} diff --git a/server/README.md b/server/README.md index 5ae2aa5..c8daa99 100644 --- a/server/README.md +++ b/server/README.md @@ -294,7 +294,7 @@ The same calendar data drives a slide-in banner on open clients: when an episode time passes and Sonarr has not imported it yet, `/v1/status` starts returning an alert — ```json -{"maintenance": false, "message": "", "alerts": [ +{"maintenance": false, "message": "", "notificationDisplay": "home_only", "alerts": [ {"id": "sonarr:7:42:aired", "kind": "sonarr-aired", "title": "Northbound", "message": "S02E04 — The Crossing aired at 9:00 PM and will be in Emby soon.", "itemId": "sonarr:7:42", "imageTag": "sonarr", "airedAt": "2026-07-27T21:00:00+12:00"}]} @@ -310,6 +310,13 @@ The server has no idea which TVs have seen what, so the client owns that: it per ids it has shown and displays each alert once, for ten seconds, without taking focus — a ring on the banner counts that down, since the viewer cannot dismiss it by remote. +The global **Notification display** setting on Gateway settings controls every viewer and +television. `everywhere` permits banners while browsing and over active playback; +`home_only` permits browsing but not movies, episodes or trailers; `off` returns no banner +alerts and the client renders none. Home only is the default for a new or older settings +document, and clients also treat a missing or unknown value as Home only. The policy rides +this status poll, so changing it takes effect on open sets without publishing an APK. + Clients poll only while a Memby screen is in the foreground, and record an alert as shown only when the banner is actually on screen. So keep offering an alert for the whole window rather than once: a TV that was showing its screensaver when the episode aired will pick diff --git a/server/internal/api/admin_gateway_settings.go b/server/internal/api/admin_gateway_settings.go index 20e577a..592eba7 100644 --- a/server/internal/api/admin_gateway_settings.go +++ b/server/internal/api/admin_gateway_settings.go @@ -11,10 +11,10 @@ import ( ) // adminGatewaySettingsResponse is deliberately three things at once: what the operator has -// chosen, what the container was started with, and what is therefore in force. A settings -// page that showed only the first would leave every empty field looking like a value of -// nothing, when an empty field here means "whatever .env says" — and the operator has no -// other way to see what that is without opening a file on the NAS. +// chosen, the deployed or server defaults, and what is therefore in force. A settings page +// that showed only the first would leave every empty environment-backed field looking like +// a value of nothing, when it means "whatever .env says" — and the operator has no other +// way to see that without opening a file on the NAS. type adminGatewaySettingsResponse struct { Settings store.GatewaySettings `json:"settings"` Deployed deployedGatewaySettings `json:"deployed"` @@ -22,7 +22,8 @@ type adminGatewaySettingsResponse struct { // LogLevels is the vocabulary rather than a list the console keeps its own copy of, // for the reason the preference catalogue is served with the accounts page: a value // the server would refuse must never be offerable. - LogLevels []string `json:"logLevels"` + LogLevels []string `json:"logLevels"` + NotificationDisplays []string `json:"notificationDisplays"` // Version and Timezone name the process this page is about, so the page can identify // the gateway it is changing without a second request. Version string `json:"version"` @@ -34,17 +35,19 @@ func (s *Server) gatewaySettingsResponse() adminGatewaySettingsResponse { Settings: settings, Deployed: s.deployedSettings(), Effective: deployedGatewaySettings{ - Timezone: s.householdTimezoneName(), - LogLevel: s.effectiveLogLevel(), - SessionIdleDays: int(s.sessionIdleExpiry() / (24 * time.Hour)), - SonarrAlertMinutes: int(s.sonarrAlertWindow() / time.Minute), - RadarrAlertMinutes: int(s.radarrAlertWindow() / time.Minute), - EmbyHealthSeconds: int(s.embyHealthInterval() / time.Second), - SlowRequestMillis: effectiveSlowRequestMillis(s.slowRequestThreshold()), - LibrarySyncMinutes: int(s.LibrarySyncInterval() / time.Minute), + Timezone: s.householdTimezoneName(), + LogLevel: s.effectiveLogLevel(), + SessionIdleDays: int(s.sessionIdleExpiry() / (24 * time.Hour)), + SonarrAlertMinutes: int(s.sonarrAlertWindow() / time.Minute), + RadarrAlertMinutes: int(s.radarrAlertWindow() / time.Minute), + NotificationDisplay: s.notificationDisplay(), + EmbyHealthSeconds: int(s.embyHealthInterval() / time.Second), + SlowRequestMillis: effectiveSlowRequestMillis(s.slowRequestThreshold()), + LibrarySyncMinutes: int(s.LibrarySyncInterval() / time.Minute), }, - LogLevels: store.GatewayLogLevels, - Version: buildinfo.Version(), + LogLevels: store.GatewayLogLevels, + NotificationDisplays: store.GatewayNotificationDisplays, + Version: buildinfo.Version(), } } @@ -94,6 +97,7 @@ func (s *Server) handleAdminGatewaySettings(w http.ResponseWriter, r *http.Reque s.loggerFor(r.Context()).Info("gateway settings changed", "timezone", s.householdTimezoneName(), "log_level", s.effectiveLogLevel(), + "notification_display", s.notificationDisplay(), "session_idle_days", int(s.sessionIdleExpiry()/(24*time.Hour)), "operator", operator) if summary := gatewaySettingsChanges(previous, stored); summary != "" { @@ -130,6 +134,9 @@ func gatewaySettingsChanges(before, after store.GatewaySettings) string { if before.RadarrAlertMinutes != after.RadarrAlertMinutes { changes = append(changes, "film alert window") } + if before.NotificationDisplay != after.NotificationDisplay { + changes = append(changes, "notification display") + } if before.EmbyHealthSeconds != after.EmbyHealthSeconds { changes = append(changes, "Emby health probe") } diff --git a/server/internal/api/gateway_settings.go b/server/internal/api/gateway_settings.go index 682d5c8..2951f8c 100644 --- a/server/internal/api/gateway_settings.go +++ b/server/internal/api/gateway_settings.go @@ -125,6 +125,17 @@ func (s *Server) radarrAlertWindow() time.Duration { s.cfg.RadarrAlertWindow) } +// notificationDisplay is the household-wide banner policy. The store normalises the +// cached value, and this fallback keeps tests and a not-yet-primed server safe too. +func (s *Server) notificationDisplay() string { + switch display := s.gatewaySettings.get().NotificationDisplay; display { + case store.NotificationDisplayEverywhere, store.NotificationDisplayOff: + return display + default: + return store.NotificationDisplayHomeOnly + } +} + // slowRequestThreshold is the line between a request that logs a breakdown and one that // does not. Switched off it returns a duration no request can exceed rather than zero, // because zero would mean *every* request carried one — the opposite of what the operator @@ -166,18 +177,20 @@ func overrideWindow(value int, unit, deployed time.Duration) time.Duration { } } -// deployedGatewaySettings describes what the container was started with, so the console -// can show the value a cleared field falls back to. Deliberately not the same shape as -// the overrides: these are facts, not choices, and nothing may write them back. +// deployedGatewaySettings describes the defaults a cleared field falls back to: deployed +// values for environment-backed settings, and Home only for the native notification +// policy. Deliberately not the same shape as the stored document: these are facts, not +// choices, and nothing may write them back. type deployedGatewaySettings struct { - Timezone string `json:"timezone"` - LogLevel string `json:"logLevel"` - SessionIdleDays int `json:"sessionIdleDays"` - SonarrAlertMinutes int `json:"sonarrAlertMinutes"` - RadarrAlertMinutes int `json:"radarrAlertMinutes"` - EmbyHealthSeconds int `json:"embyHealthSeconds"` - SlowRequestMillis int `json:"slowRequestMillis"` - LibrarySyncMinutes int `json:"librarySyncMinutes"` + Timezone string `json:"timezone"` + LogLevel string `json:"logLevel"` + SessionIdleDays int `json:"sessionIdleDays"` + SonarrAlertMinutes int `json:"sonarrAlertMinutes"` + RadarrAlertMinutes int `json:"radarrAlertMinutes"` + NotificationDisplay string `json:"notificationDisplay"` + EmbyHealthSeconds int `json:"embyHealthSeconds"` + SlowRequestMillis int `json:"slowRequestMillis"` + LibrarySyncMinutes int `json:"librarySyncMinutes"` } func (s *Server) deployedSettings() deployedGatewaySettings { @@ -186,14 +199,15 @@ func (s *Server) deployedSettings() deployedGatewaySettings { timezone = s.cfg.SonarrLocation.String() } return deployedGatewaySettings{ - Timezone: timezone, - LogLevel: levelName(s.deployedLogLevel), - SessionIdleDays: int(s.cfg.SessionIdleExpiry / (24 * time.Hour)), - SonarrAlertMinutes: int(s.cfg.SonarrAlertWindow / time.Minute), - RadarrAlertMinutes: int(s.cfg.RadarrAlertWindow / time.Minute), - EmbyHealthSeconds: int(s.cfg.EmbyHealthInterval / time.Second), - SlowRequestMillis: int(s.cfg.SlowRequestThreshold / time.Millisecond), - LibrarySyncMinutes: int(s.cfg.SyncInterval / time.Minute), + Timezone: timezone, + LogLevel: levelName(s.deployedLogLevel), + SessionIdleDays: int(s.cfg.SessionIdleExpiry / (24 * time.Hour)), + SonarrAlertMinutes: int(s.cfg.SonarrAlertWindow / time.Minute), + RadarrAlertMinutes: int(s.cfg.RadarrAlertWindow / time.Minute), + NotificationDisplay: store.NotificationDisplayHomeOnly, + EmbyHealthSeconds: int(s.cfg.EmbyHealthInterval / time.Second), + SlowRequestMillis: int(s.cfg.SlowRequestThreshold / time.Millisecond), + LibrarySyncMinutes: int(s.cfg.SyncInterval / time.Minute), } } diff --git a/server/internal/api/gateway_settings_test.go b/server/internal/api/gateway_settings_test.go index 70f6470..c2c7c9e 100644 --- a/server/internal/api/gateway_settings_test.go +++ b/server/internal/api/gateway_settings_test.go @@ -54,6 +54,17 @@ func TestGatewaySettingsChangesReportsOnlyWhatMoved(t *testing.T) { } } +func TestNotificationDisplayDefaultsToHomeOnly(t *testing.T) { + s := &Server{} + if got := s.notificationDisplay(); got != store.NotificationDisplayHomeOnly { + t.Fatalf("unset notification display = %q, want %q", got, store.NotificationDisplayHomeOnly) + } + s.gatewaySettings.set(store.GatewaySettings{NotificationDisplay: store.NotificationDisplayEverywhere}) + if got := s.notificationDisplay(); got != store.NotificationDisplayEverywhere { + t.Fatalf("notification display = %q, want everywhere", got) + } +} + func TestLevelNameCoversTheVocabulary(t *testing.T) { for _, level := range store.GatewayLogLevels { if got := levelName(parseTestLevel(t, level)); got != level { diff --git a/server/internal/api/maintenance.go b/server/internal/api/maintenance.go index d7a44a1..be446a4 100644 --- a/server/internal/api/maintenance.go +++ b/server/internal/api/maintenance.go @@ -109,9 +109,10 @@ func (s *Server) handleServiceStatus(w http.ResponseWriter, r *http.Request, ses message = store.DefaultMaintenanceMessage } alerts := []clientAlert{} + notificationDisplay := s.notificationDisplay() // Nothing to celebrate while the service is down, and the client is showing the // maintenance screen anyway. - if !state.Enabled { + if !state.Enabled && notificationDisplay != store.NotificationDisplayOff { published, sonarr := s.publishedAlerts(r.Context()), s.sonarrAiredAlerts(r.Context()) if len(published) > 0 || len(sonarr) > 0 { prefs, err := s.notificationPreferencesFor(r.Context(), sess.EmbyUserID) @@ -136,6 +137,7 @@ func (s *Server) handleServiceStatus(w http.ResponseWriter, r *http.Request, ses "quietTime": quiet.Active, "message": message, "alerts": alerts, + "notificationDisplay": notificationDisplay, "compatible": compatible, "compatibilityMessage": compatibilityMessage, "clientVersion": clientVersion(r), diff --git a/server/internal/store/gateway_settings.go b/server/internal/store/gateway_settings.go index e1871c3..7994ed3 100644 --- a/server/internal/store/gateway_settings.go +++ b/server/internal/store/gateway_settings.go @@ -17,13 +17,14 @@ const GatewaySettingsKey = "gateway_settings" // GatewaySettings is the handful of server-level decisions an operator can change // without a redeployment. // -// Every field is an *override* of the value the container was started with, and the zero -// value means "whatever was deployed". That is the whole design: `.env` remains the +// Most fields are an *override* of the value the container was started with, and their +// zero value means "whatever was deployed". That is the whole design: `.env` remains the // configuration — it is what a fresh container comes up with, what deploy-server.ps1 // writes and what an operator locked out of the console still has — and this row is a // runtime amendment to it, so the console can always show the deployed value beside the // one in force and an operator can put a setting back by clearing it rather than by -// remembering what it used to be. +// remembering what it used to be. NotificationDisplay is the exception: it is a native +// server policy with a Home-only default rather than an environment-backed value. // // A window that can legitimately be *off* therefore needs a value distinct from "not // overridden", which is why the two alert windows and the health interval take -1 for off @@ -44,6 +45,10 @@ type GatewaySettings struct { // added" banner stays on offer to a television that was switched off at the time. SonarrAlertMinutes int `json:"sonarrAlertMinutes"` RadarrAlertMinutes int `json:"radarrAlertMinutes"` + // NotificationDisplay is where informational banners may be drawn across every + // viewer and television. It is server-owned rather than a per-viewer preference: + // one household should not have different interruption rules on different sets. + NotificationDisplay string `json:"notificationDisplay"` // EmbyHealthSeconds is how often the reachability probe asks Emby whether it is there. EmbyHealthSeconds int `json:"embyHealthSeconds"` // SlowRequestMillis is how long a request must take before its log line carries a @@ -74,6 +79,20 @@ const GatewaySettingsOff = -1 // and refusing is worth nothing if the list it refuses against lives somewhere else. var GatewayLogLevels = []string{"trace", "debug", "info", "warn", "error"} +const ( + NotificationDisplayEverywhere = "everywhere" + NotificationDisplayHomeOnly = "home_only" + NotificationDisplayOff = "off" +) + +// GatewayNotificationDisplays is the complete wire vocabulary, served to the console so +// it cannot offer a value the gateway would refuse. +var GatewayNotificationDisplays = []string{ + NotificationDisplayEverywhere, + NotificationDisplayHomeOnly, + NotificationDisplayOff, +} + func normalizeGatewaySettings(settings GatewaySettings) GatewaySettings { settings.Timezone = strings.TrimSpace(settings.Timezone) if settings.Timezone != "" { @@ -91,6 +110,13 @@ func normalizeGatewaySettings(settings GatewaySettings) GatewaySettings { settings.SessionIdleDays = clampOverride(settings.SessionIdleDays, 1, 3650, false) settings.SonarrAlertMinutes = clampOverride(settings.SonarrAlertMinutes, 1, 24*60, true) settings.RadarrAlertMinutes = clampOverride(settings.RadarrAlertMinutes, 1, 7*24*60, true) + settings.NotificationDisplay = strings.ToLower(strings.TrimSpace(settings.NotificationDisplay)) + if !containsString(GatewayNotificationDisplays, settings.NotificationDisplay) { + // Missing covers both a new installation and a row written by an older gateway. + // Unknown is equally conservative: silence from the setting must never authorise + // an interruption over active playback. + settings.NotificationDisplay = NotificationDisplayHomeOnly + } settings.EmbyHealthSeconds = clampOverride(settings.EmbyHealthSeconds, 10, 3600, true) // The floor is 1ms rather than 0 because "breakdown on everything" is a real thing to // want for a few minutes, and off is a real thing to want too — this is the one @@ -133,7 +159,7 @@ func (s *Store) GatewaySettings(ctx context.Context) (GatewaySettings, error) { err := s.pool.QueryRow(ctx, `SELECT value FROM app_settings WHERE key = $1`, GatewaySettingsKey).Scan(&raw) if errors.Is(err, pgx.ErrNoRows) { - return GatewaySettings{}, nil + return normalizeGatewaySettings(GatewaySettings{}), nil } if err != nil { return GatewaySettings{}, fmt.Errorf("store: read gateway settings: %w", err) diff --git a/server/internal/store/gateway_settings_test.go b/server/internal/store/gateway_settings_test.go index a97ddbf..3faa263 100644 --- a/server/internal/store/gateway_settings_test.go +++ b/server/internal/store/gateway_settings_test.go @@ -21,6 +21,25 @@ func TestNormalizeGatewaySettingsRefusesWhatItCannotUse(t *testing.T) { } } +func TestNormalizeGatewaySettingsDefaultsNotificationDisplayToHomeOnly(t *testing.T) { + for _, value := range []string{"", "unknown"} { + settings := normalizeGatewaySettings(GatewaySettings{NotificationDisplay: value}) + if settings.NotificationDisplay != NotificationDisplayHomeOnly { + t.Fatalf("notification display %q became %q, want %q", + value, settings.NotificationDisplay, NotificationDisplayHomeOnly) + } + } +} + +func TestNormalizeGatewaySettingsKeepsNotificationDisplayVocabulary(t *testing.T) { + for _, value := range GatewayNotificationDisplays { + settings := normalizeGatewaySettings(GatewaySettings{NotificationDisplay: " " + value + " "}) + if settings.NotificationDisplay != value { + t.Fatalf("notification display %q became %q", value, settings.NotificationDisplay) + } + } +} + func TestNormalizeGatewaySettingsKeepsTheThreeMeanings(t *testing.T) { settings := normalizeGatewaySettings(GatewaySettings{ Timezone: "Pacific/Auckland", LogLevel: " Debug ",