diff --git a/CLAUDE.md b/CLAUDE.md index a000320..e1fc92e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -488,6 +488,55 @@ behind it. Things to preserve: enabling the scanner would announce every show that had already ended as new news, and without durable history a gateway restart could announce the same change again. +**Watch time is Tracearr's, and it is never counted twice.** `store.watchedMsExpr` in +`internal/store/watch_time.go` is the one definition of "how long was this actually watched" +— the greater of Tracearr's `durationMs` and `progressMs`, capped at the title's own length — +and the console's figure and the viewer's summary are both queries over `tracearr_sessions` +rather than a second table of minutes. A table counting watching separately would be a copy +of a copy, wrong the moment Tracearr corrects a session. It is read two ways: + +- **The console reads it beside the person.** `/admin/api/accounts` carries a `watchTime` + per account — week, month, lifetime, and when they last watched — from one grouped query + for the whole household, because that page grows with the family. `matched` is the + load-bearing field: a household running no Tracearr and a viewer Tracearr has never seen + both arrive as zeroes, and a console drawing those as "0 min this week" would have an + operator investigating a person rather than an integration. A watch-time read that fails + costs the figures and never the account list. +- **`attributeWatchTime` joins the two systems on the username**, which is the identity they + genuinely share, and prefers the Tracearr id `recommendation_user_profiles` recorded where + there is one — so a viewer renamed in one system keeps their figures instead of silently + reporting zero. It is pure, so the console and the digest cannot attribute the same rows + differently. +- **The weekly summary is a personal notification, not a service alert.** A service alert is + the house being told something; how long somebody watched is nobody else's news, so it + lands in My Alerts (`watch-time-week` / `watch-time-month`, which an app that predates them + renders with the fallback icon) and follows the person to every set. `RegisterWatchTimeTasks` + registers it as an ordinary scheduler job, so an operator can see when it last ran and send + one by hand — which for a job that fires once a week is the difference between "it has sent + nothing" and "it has not run". +- **The source key is the only thing preventing a repeat.** It runs hourly and sends from the + appointed hour to the end of that day, because `watch-time:weekly:2026-W33` is written + `ON CONFLICT DO NOTHING`: a container restarted three times on a Sunday evening delivers one + summary, and a gateway that was off all evening still delivers it the next hour it is up. + The monthly summary is the same trick over a `YYYY-MM` key, which is why it catches up + rather than being skipped for ever by a gateway that was down on the first. +- **Sunday evening, not Monday morning**, because the figure sent is week-to-date: on a Monday + it would summarise almost nothing. Every boundary is a household-local *calendar* date + (`weekStartIn`, `monthStartIn`, `previousMonth`), never `now.Add(-7*24*time.Hour)` — a week + containing a daylight-saving change is 23 or 25 hours short or long, and subtracting hours + puts the boundary an hour inside the previous Sunday twice a year. `watch_time_test.go` pins + the clock-change week. +- **Two switches, and they answer different questions.** `watch_time_digest` in the + `featureCatalogue` is the household's and carries no capability — nothing on the television + has to understand this. `NotificationPreferences.WatchTimeDigest` is the viewer's own, kept + apart from `SystemAlerts` because this is the only notification there that is about *them*. + Turning it off also withdraws the summaries already sitting in their list + (`filterStoredNotifications`): switching a weekly notice off is a statement about the ones + already there as much as about the next one. +- **Nothing under `watchTimeDigestFloor` is sent**, and a preference that will not load is + read as "not now" rather than as consent. A digest reporting four minutes is a notification + about a title somebody abandoned, and a feed carrying those is one nobody opens. + **Search** (`ui/search/`) is a two-pane instant-search destination on the rail: a fixed 6×6 on-screen keyboard on the left, a results grid on the right that updates as you type. Nothing is ever "submitted". `SearchViewModel` runs one pipeline — `debounce(250)` → @@ -1424,6 +1473,70 @@ but the address. Four things to preserve: than none, since it opens a connection nothing will use and leaves the one that matters cold. +**An advance opens the next episode's stream before anybody asks for it.** `StreamWarmer` +warms the *host*; this warms the title, which it can only do here because an advance is the +one case where the app knows what is next minutes ahead — `NextUpResolver` has the answer +five minutes before the credits (`NEXT_UP_STREAM_WARM_LEAD_MS`). Media3's +`DefaultPreloadManager` does the work; `ui/player/NextEpisodePreloader.kt` owns the two +decisions it cannot make for itself, and `PreloadPlan.kt` holds them as pure functions so +they can be pinned by plain JUnit. Things to preserve: + +- **The player and the manager are built from one builder.** + `DefaultPreloadManager.Builder.buildExoPlayer` *overwrites* the media source factory, + renderers, load control, bandwidth meter, track selector and playback looper on whatever + `ExoPlayer.Builder` it is handed, so everything shared is set on the manager's builder and + the player's carries only what the manager has no opinion about. The looper is the one + that would actually break: a source prepared on one playback thread and played on another + is a crash, not a slow start. +- **Ranking data is a position in the journey, not a playlist index.** Nothing here is a + playlist — the player is handed one episode at a time and the next is discovered while it + plays — so a rank is assigned when an answer arrives and only ever moves forward. Exactly + one episode ahead is preloaded (`preloadTargetFor`); two would double the cost for a + viewer who has two episodes' worth of time to walk away. +- **Eviction never touches the episode playing.** On an advance the player has just been + handed that episode's `MediaSource`, and `remove` releases the source underneath the + decoder using it. `obsoletePreloadRanks` is strictly-behind for that reason, `advanceTo` + is called *after* `startMedia` and not before it, and the entry for the playing episode is + left in the manager to be quietened by its target status turning to + `PRELOAD_STATUS_NOT_PRELOADED` on the next `invalidate`. +- **The bound is a memory ceiling first.** `PRELOAD_RANGE_MS` is five seconds because the + expensive half of starting a stream is the connection, the container header and the seek + index — which `specifiedRangeLoaded` pays by preparing the source and selecting tracks — + not the bytes. A 4K direct play runs past 30 Mbps, so every second held ahead is megabytes + on a box with none spare, for a title the viewer may not go on to. +- **Registration hangs off the resolver, not off one call site.** `NextUpResolver`'s + `onResolved` fires for the first lookup *and* for every re-negotiation of a stale stream, + and a re-negotiation is exactly when preloaded work stops matching the URL the player will + be handed. It fires only for the episode still playing, or an answer that arrived after + the viewer moved on would have the preloader open a connection for a journey that no + longer exists. +- **Every part of it degrades to what came before.** A manager that will not build leaves + the preloader unattached, `sourceFor` answers null and `startMedia` takes the ordinary + `setMediaItem` path — which is also what a cold start, the direct-to-Emby path, a retry + that re-negotiated, and an unfinished preload all take. That fallback is *logged* + (`event=preload_unavailable`), because it is invisible from the viewer's side and a set + that never preloads anything otherwise looks exactly like one where the feature works and + never happens to save time. +- **`PRELOADING_ENABLED` and `DYNAMIC_SCHEDULING_ENABLED` are separate switches** in + `PlayerEngine`, the `THEME_PICKER_ENABLED` precedent, so a television that misbehaves on + Media3's experimental scheduling can have that taken away without losing preloading or the + version bump underneath both. +- **`event=first_frame` says which start it is measuring** (`start=cold` / `start=preloaded`). + The whole feature is a claim about one of two latencies, and a log that could not separate + them could not show whether it worked. `event=preload_ready` carries how long the preload + itself took and the range it was bounded to. + +**Media3 is one version across every artifact, and the Jellyfin FFmpeg extension pins which +one that can be.** That extension is compiled against `media3-exoplayer` and reached +*reflectively* through `EXTENSION_RENDERER_MODE_ON`, so a core from a different minor line +fails at renderer construction rather than at compile time — and `PlayerEngine`'s +`LinkageError` fallback would swallow it, silently withdrawing surround software decode with +nothing in the log to say why. Jellyfin publishes up to the 1.9 line, so `media3Version` in +`app/build.gradle.kts` is on it. `enablePerStreamMediaProgression` arrived in 1.11 and is +therefore not available here; `experimentalSetDynamicSchedulingEnabled` is the part of that +same work which is. Moving the core past 1.9 means finding a matching extension first, or +deciding to do without DTS. + **Playback position has one ordered exit path.** Ten-second progress updates, pause/seek updates and the final Stop all pass through `EmbyRepository`'s `playbackReportMutex`, so a slow older Progress request cannot complete after Stop and move Emby's saved playhead back. diff --git a/admin-ui/dist/assets/index-CahtXjpP.js b/admin-ui/dist/assets/index-CahtXjpP.js deleted file mode 100644 index ad1d363..0000000 --- a/admin-ui/dist/assets/index-CahtXjpP.js +++ /dev/null @@ -1,11 +0,0 @@ -import{r as x,a as $s,u as Ve,L as ie,b as Be,m as Fe,N as ps,O as Ts,c as $e,B as Ls,R as Ds,d as V,e as qs}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 d of a)if(d.type==="childList")for(const t of d.addedNodes)t.tagName==="LINK"&&t.rel==="modulepreload"&&i(t)}).observe(document,{childList:!0,subtree:!0});function o(a){const d={};return a.integrity&&(d.integrity=a.integrity),a.referrerPolicy&&(d.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?d.credentials="include":a.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function i(a){if(a.ep)return;a.ep=!0;const d=o(a);fetch(a.href,d)}})();var xs={exports:{}},Te={};/** - * @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 Fs=x,Ps=Symbol.for("react.element"),Os=Symbol.for("react.fragment"),Us=Object.prototype.hasOwnProperty,Vs=Fs.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Bs={key:!0,ref:!0,__self:!0,__source:!0};function js(s,n,o){var i,a={},d=null,t=null;o!==void 0&&(d=""+o),n.key!==void 0&&(d=""+n.key),n.ref!==void 0&&(t=n.ref);for(i in n)Us.call(n,i)&&!Bs.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:Ps,type:s,key:d,ref:t,props:a,_owner:Vs.current}}Te.Fragment=Os;Te.jsx=js;Te.jsxs=js;xs.exports=Te;var e=xs.exports,vs,ze=$s;vs=ze.createRoot,ze.hydrateRoot;const bs={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",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 ee({name:s,className:n}){const o=bs[s];return o?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:o})}):null}function Le({name:s,tone:n}){return bs[s]?e.jsx("span",{className:"glyph","data-tone":n,children:e.jsx(ee,{name:s})}):null}const ge=[{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:"Memby 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:"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:"Server logs",title:"Server 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:"recommendations",path:"/admin/recommendations",label:"For You",title:"For You",intro:"The prepared pools personalised rows are drawn from.",icon:"sparkle"},{id:"ratings",path:"/admin/ratings",label:"Movie ratings",title:"Movie ratings",intro:"Optional MDBList scores on films and shows.",icon:"star"},{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:"imports",path:"/admin/imports",label:"Imports",title:"Imports",intro:"Catalogue synchronisation history.",icon:"database"},{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",path:"/admin/integrations",label:"Integrations",title:"Integrations",intro:"Send administrative events to Discord and, in time, elsewhere.",icon:"plug"}]},{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 the household has been looking for, and what it 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"}]}],_s=ge.flatMap(s=>s.items),Ws=ge.flatMap(s=>s.items.filter(n=>!n.path.includes(":")).map(n=>({...n,group:s.label??""})));class He extends Error{constructor(n,o){super(n),this.status=o,this.name="ApiError"}}const zs=5*60*1e3;let gs=Date.now();for(const s of["pointerdown","pointermove","keydown","wheel","scroll"])window.addEventListener(s,()=>{gs=Date.now()},{passive:!0});const Hs=()=>Date.now()-gs({}));throw new He(i.error??`Request failed (${o.status})`,o.status)}if(o.status!==204)return await o.json()}function Se(s){const n=new URLSearchParams;for(const[i,a]of Object.entries(s))a==null||a===""||a===!1||n.set(i,String(a));const o=n.toString();return o?`?${o}`:""}const q={get:s=>Ce(s),post:(s,n)=>Ce(s,{method:"POST",body:n===void 0?void 0:JSON.stringify(n)}),put:(s,n)=>Ce(s,{method:"PUT",body:n===void 0?void 0:JSON.stringify(n)}),del:s=>Ce(s,{method:"DELETE"})},Gs=3e4,fs=x.createContext(null);function Zs({children:s}){var h;const[n,o]=x.useState(),[i,a]=x.useState(!1),[d,t]=x.useState(""),[r,c]=x.useState(""),[l,g]=x.useState(!0),v=x.useRef(0),u=x.useCallback(async()=>{const j=++v.current;try{const b=await q.get("/admin/api/status");if(j!==v.current)return;o(b),a(!0),c("")}catch(b){if(j!==v.current)return;a(!1),c(b instanceof Error?b.message:String(b))}finally{j===v.current&&(t(new Date().toISOString()),g(!1))}},[]),m=x.useCallback(async j=>{var b;await q.post("/admin/api/maintenance",{enabled:j,message:((b=n==null?void 0:n.maintenance)==null?void 0:b.message)??""}),await u()},[u,(h=n==null?void 0:n.maintenance)==null?void 0:h.message]);x.useEffect(()=>{u();let j;const b=()=>{window.clearInterval(j),j=document.hidden?void 0:window.setInterval(()=>void u(),Gs)},k=()=>{b(),document.hidden||u()};return b(),document.addEventListener("visibilitychange",k),()=>{window.clearInterval(j),document.removeEventListener("visibilitychange",k)}},[u]);const f=x.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:i,checkedAt:d,error:r,loading:l,reload:u,setMaintenance:m}},[n,i,d,r,l,u,m]);return e.jsx(fs.Provider,{value:f,children:s})}function ae(){const s=x.useContext(fs);if(!s)throw new Error("useGateway used outside GatewayProvider");return s}function Js(s,n){const o=s.label.toLowerCase(),i=s.group.toLowerCase();return o.startsWith(n)?4:o.includes(n)?3:i.includes(n)?2:`${s.title} ${s.intro}`.toLowerCase().includes(n)?1:0}function Qs(){const s=Ve(),{status:n}=ae(),[o,i]=x.useState(!1),[a,d]=x.useState(""),[t,r]=x.useState(0),c=x.useRef(null),l=x.useRef(null),g=x.useMemo(()=>{const u=a.trim().toLowerCase();return[...Ws,...((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:u?Js(f,u):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}))},[a,n]);x.useEffect(()=>r(0),[a]),x.useEffect(()=>{const u=m=>{var f;(f=c.current)!=null&&f.contains(m.target)||i(!1)};return document.addEventListener("pointerdown",u),()=>document.removeEventListener("pointerdown",u)},[]),x.useEffect(()=>{const u=m=>{var h,j;if(m.key!=="S"||!m.shiftKey||m.ctrlKey||m.metaKey||m.altKey)return;const f=document.activeElement;f&&(f.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(f.tagName))||(m.preventDefault(),(h=l.current)==null||h.focus(),(j=l.current)==null||j.select())};return window.addEventListener("keydown",u),()=>window.removeEventListener("keydown",u)},[]);const v=u=>{var m;i(!1),d(""),(m=l.current)==null||m.blur(),s(u)};return e.jsxs("div",{className:"omni",ref:c,"data-open":o||void 0,children:[e.jsxs("div",{className:"omni-input",children:[e.jsx(ee,{name:"search"}),e.jsx("input",{ref:l,type:"search",value:a,placeholder:"Search pages, users and devices…","aria-label":"Search pages, users and devices","aria-expanded":o,onFocus:()=>i(!0),onChange:u=>{d(u.target.value),i(!0)},onKeyDown:u=>{var m;if(u.key==="Escape")d(""),i(!1),(m=l.current)==null||m.blur();else if(u.key==="ArrowDown")u.preventDefault(),r(f=>Math.min(f+1,g.length-1));else if(u.key==="ArrowUp")u.preventDefault(),r(f=>Math.max(f-1,0));else if(u.key==="Enter"){const f=g[t];if(!f)return;u.preventDefault(),v(f.item.path)}}}),e.jsx("span",{className:"omni-key",children:"⇧S"})]}),o?e.jsx("div",{className:"omni-panel",role:"listbox",children:g.length===0?e.jsx("p",{className:"empty",children:"No pages, users or devices match that search."}):g.map((u,m)=>e.jsxs("a",{className:m===t?"omni-item on":"omni-item",href:u.item.path,role:"option","aria-selected":m===t,onPointerEnter:()=>r(m),onClick:f=>{f.preventDefault(),v(u.item.path)},children:[u.item.icon?e.jsx(ee,{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(),P=s=>s?new Date(s).toLocaleString():"—";function be(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 o=Math.floor(n/60);return o<60?`${o}m ${n%60}s`:`${Math.floor(o/60)}h ${o%60}m`}function Pe(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 o=Math.round(n/60);return o<48?o===1?"hourly":`every ${o} hours`:`every ${Math.round(o/24)} days`}function ye(s){const n=["B","KB","MB","GB"];let o=Number(s??0),i=0;for(;o>=1024&&iString(s??"?").trim().split(/\s+/).slice(0,2).map(n=>n[0]??"").join("").toUpperCase(),Re=s=>`${Math.round((s??0)*100)}%`;function pe(s){if(!s)return"—";const n=Date.now()-new Date(s).getTime();if(n<0)return"just now";const o=Math.floor(n/1e3);if(o<45)return"just now";const i=Math.floor(o/60);if(i<60)return`${i} min ago`;const a=Math.floor(i/60);if(a<24)return`${a}h ago`;const d=Math.floor(a/24);return d<30?`${d}d ago`:new Date(s).toLocaleDateString()}const ws=15*60*1e3,Ys=3*60*60*1e3,Ne=s=>!!s&&Date.now()-new Date(s).getTime(){var b;try{const k=await q.get(`/admin/api/notifications?limit=${Ge}`);o(k.events),a(k.unread),t(k.types),v.current=Math.max(v.current,((b=k.events[0])==null?void 0:b.id)??0),g("")}catch(k){g(k instanceof Error?k.message:String(k))}},[]),m=x.useCallback(b=>{v.current=Math.max(v.current,b.id),o(k=>k.some(p=>p.id===b.id)?k:[b,...k].sort((p,y)=>y.id-p.id).slice(0,Ge)),b.readAt||a(k=>k+1),t(k=>k.some(p=>p.type===b.type)?k.map(p=>p.type===b.type?{...p,count:p.count+1}:p):[...k,{type:b.type,count:1}])},[]);x.useEffect(()=>{u()},[u]),x.useEffect(()=>{let b=null,k,p=!1;return(()=>{p||(b=new EventSource(`/admin/api/notifications/stream?after=${v.current}`),b.addEventListener("open",()=>{c(!0),window.clearInterval(k),k=void 0}),b.addEventListener("admin",E=>{try{m(JSON.parse(E.data))}catch{}}),b.addEventListener("error",()=>{c(!1),k===void 0&&(k=window.setInterval(()=>void u(),Xs))}))})(),()=>{p=!0,b==null||b.close(),window.clearInterval(k)}},[m,u]);const f=x.useCallback(async b=>{const k=b.filter(p=>p>0);if(k.length!==0){o(p=>p.map(y=>k.includes(y.id)&&!y.readAt?{...y,readAt:new Date().toISOString()}:y));try{const p=await q.post("/admin/api/notifications/read",{ids:k});a(p.unread)}catch{u()}}},[u]),h=x.useCallback(async()=>{a(0),o(b=>b.map(k=>k.readAt?k:{...k,readAt:new Date().toISOString()}));try{const b=await q.post("/admin/api/notifications/read",{all:!0});a(b.unread)}catch{u()}},[u]),j=x.useMemo(()=>({events:n,unread:i,types:d,connected:r,error:l,markRead:f,markAllRead:h,reload:u}),[n,i,d,r,l,f,h,u]);return e.jsx(ks.Provider,{value:j,children:s})}function We(){const s=x.useContext(ks);if(!s)throw new Error("useNotifications used outside NotificationProvider");return s}function Ns(s){return s.severity==="error"?"bad":s.severity==="warning"?"warn":"info"}function Ss(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 Ze(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 Je=99;function sn(){const{events:s,unread:n,connected:o,markRead:i,markAllRead:a}=We(),[d,t]=x.useState(!1),r=x.useRef(null);return x.useEffect(()=>{const c=g=>{var v;(v=r.current)!=null&&v.contains(g.target)||t(!1)},l=g=>{g.key==="Escape"&&t(!1)};return document.addEventListener("pointerdown",c),document.addEventListener("keydown",l),()=>{document.removeEventListener("pointerdown",c),document.removeEventListener("keydown",l)}},[]),x.useEffect(()=>{if(!d)return;const c=s.filter(l=>!l.readAt).map(l=>l.id);c.length>0&&i(c)},[d]),e.jsxs("div",{className:"bell",ref:r,"data-open":d||void 0,children:[e.jsxs("button",{type:"button",className:"bell-button","aria-label":n>0?`Activity, ${n} unread`:"Activity","aria-expanded":d,onClick:()=>t(c=>!c),children:[e.jsx(ee,{name:"bell"}),n>0?e.jsx("span",{className:"bell-badge",children:n>Je?`${Je}+`:n}):null]}),d?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:[o?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 l=e.jsxs(e.Fragment,{children:[e.jsx(Le,{name:Ss(c.type),tone:Ns(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:pe(c.occurredAt)})]})]});return c.link?e.jsx(ie,{className:"bell-item","data-unread":!c.readAt||void 0,to:c.link,onClick:()=>t(!1),children:l},c.id):e.jsx("div",{className:"bell-item","data-unread":!c.readAt||void 0,children:l},c.id)})}),e.jsx("div",{className:"bell-foot",children:e.jsx(ie,{to:"/admin/activity",onClick:()=>t(!1),children:"All activity"})})]}):null]})}function O({title:s,intro:n,actions:o,crumbs:i,icon:a}){var r;const d=Be(),t=a??((r=_s.find(c=>Fe({path:c.path,end:!0},d.pathname)))==null?void 0:r.icon);return 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:[t?e.jsx("span",{className:"page-head-icon","aria-hidden":"true",children:e.jsx(ee,{name:t})}):null,e.jsxs("div",{className:"page-head-text",children:[e.jsx("h1",{children:s}),n?e.jsx("p",{children:n}):null]})]}),o?e.jsx("div",{className:"page-head-actions",children:o}):null]})]})}function R({title:s,intro:n,icon:o,tone:i,actions:a,footer:d,children:t}){return e.jsxs("section",{className:"card",children:[s?e.jsxs("div",{className:"card-head",children:[o?e.jsx(Le,{name:o,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,t,d?e.jsx("div",{className:"card-foot",children:d}):null]})}function oe({cols:s,children:n}){return e.jsx("div",{className:"grid","data-cols":s,children:n})}function re({tiles:s}){return e.jsx("div",{className:"tiles",children:s.map(n=>e.jsxs("div",{className:"tile",children:[n.icon?e.jsx(Le,{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 C({children:s,tone:n}){return e.jsx("span",{className:"tag","data-tone":n,children:s})}function te({children:s,tone:n}){return e.jsx("span",{className:"chip","data-tone":n,children:s})}function Z({children:s}){return e.jsx("p",{className:"empty",children:s})}function se({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 ue({children:s,tone:n}){return e.jsx("p",{className:"note","data-tone":n,children:s})}function A({children:s,onClick:n,variant:o,size:i,disabled:a,busy:d,icon:t,type:r="button",title:c}){return e.jsxs("button",{type:r,className:"","data-variant":o,"data-size":i,disabled:a||d,onClick:n,title:c,children:[d?e.jsx("span",{className:"spinner"}):t?e.jsx(ee,{name:t}):null,s]})}function D({label:s,hint:n,children:o,grow:i}){return e.jsxs("label",{className:i?"field grow":"field",children:[e.jsx("span",{children:s}),o,n?e.jsx("small",{children:n}):null]})}function z({label:s,hint:n,checked:o,onChange:i,disabled:a}){return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:o,disabled:a,onChange:d=>i(d.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 Ie({value:s,options:n,onChange:o}){return e.jsx("div",{className:"segments",role:"group",children:n.map(i=>e.jsx("button",{type:"button","aria-pressed":i.value===s,onClick:()=>o(i.value),children:i.label},String(i.value)))})}function G({children:s}){return e.jsx("div",{className:"table-wrap",children:s})}function Cs({data:s,labelOf:n,valueOf:o,toneOf:i,title:a}){if(s.length===0)return e.jsx(Z,{children:"Nothing in this window."});const d=s.map(r=>o(r)),t=Math.max(1,...d);return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"bars",children:s.map((r,c)=>{const l=o(r);return e.jsx("div",{className:"bar","data-tone":i==null?void 0:i(r),"data-empty":l===0||void 0,style:{height:`${Math.max(2,l/t*100)}%`},title:a?a(r):`${n(r,c)}: ${l}`},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 nn({value:s,total:n,tone:o}){const i=n>0?Math.min(1,s/n):0;return e.jsx("div",{className:"meter","data-tone":o,children:e.jsx("div",{style:{width:`${i*100}%`}})})}function U({message:s,onDismiss:n}){return s?e.jsxs("div",{className:"banner",role:"alert",children:[e.jsx(ee,{name:"alert"}),e.jsx("span",{children:s}),n?e.jsx("button",{type:"button",onClick:n,"aria-label":"Dismiss",children:e.jsx(ee,{name:"close"})}):null]}):null}function W({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,o)=>e.jsx("span",{className:"skeleton"},o))}),Array.from({length:s},(n,o)=>e.jsxs("section",{className:"loading-card",children:[e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"})]},o))]})}function he({title:s,body:n,confirmLabel:o="Confirm",destructive:i,busy:a,onConfirm:d,onCancel:t}){const r=x.useId(),c=x.useRef(null);return x.useEffect(()=>{var g;(g=c.current)==null||g.focus();const l=v=>{v.key==="Escape"&&t()};return document.addEventListener("keydown",l),()=>document.removeEventListener("keydown",l)},[t]),e.jsx("div",{className:"scrim",onPointerDown:l=>l.target===l.currentTarget&&t(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true","aria-labelledby":r,ref:c,tabIndex:-1,children:[e.jsx("h2",{id:r,children:s}),e.jsx("p",{children:n}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(A,{onClick:t,variant:"quiet",children:"Cancel"}),e.jsx(A,{onClick:d,variant:i?"danger":"primary",busy:a,children:o})]})]})})}function Oe({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 Ms({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 tn({open:s,onNavigate:n}){const{unread:o}=We(),i=Be(),[a,d]=x.useState(()=>{try{return JSON.parse(localStorage.getItem("memby-admin-nav")??"{}")}catch{return{}}}),t=c=>{try{localStorage.setItem("memby-admin-nav",JSON.stringify(c))}catch{}};x.useEffect(()=>{const c=ge.find(l=>l.items.some(g=>Fe({path:g.path,end:!0},i.pathname)));c&&d(l=>{const g={...l};return ge.forEach(v=>{v.collapsible!==!1&&(g[v.id]=v.id!==c.id)}),t(g),g})},[i.pathname]);const r=(c,l=!1)=>{d(g=>{const v={...g},u=!(g[c]??l);return ge.forEach(m=>{m.collapsible!==!1&&(v[m.id]=m.id===c?u:!0)}),t(v),v})};return e.jsx("nav",{className:"rail",id:"rail","data-open":s||void 0,"aria-label":"Console sections",children:ge.map(c=>{const l=c.items.filter(m=>!m.hidden);if(l.length===0)return null;const g=l.some(m=>Fe({path:m.path,end:!0},i.pathname)),v=c.collapsible!==!1,u=g||!v||!(a[c.id]??c.defaultCollapsed??!1);return e.jsxs("div",{className:"rail-group",children:[c.label&&v?e.jsxs("button",{type:"button",className:"rail-head","aria-expanded":u,onClick:()=>r(c.id,c.defaultCollapsed),children:[c.label,e.jsx(ee,{name:"caret",className:"ico caret"})]}):c.label?e.jsx("div",{className:"rail-head rail-head-static",children:c.label}):null,u?l.map(m=>e.jsx(ps,{to:m.path,end:m.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:[m.icon?e.jsx(ee,{name:m.icon}):null,m.label,m.badge==="notifications"&&o>0?e.jsx("span",{className:"rail-badge",children:o>99?"99+":o}):null]})},m.id)):null]},c.id)})})}function an(){var E,N,B;const{version:s,currentUser:n,online:o,loading:i,status:a,setMaintenance:d}=ae(),[t,r]=x.useState(!1),[c,l]=x.useState(!1),[g,v]=x.useState(!1),[u,m]=x.useState(!1),f=Be(),h=!!((E=a==null?void 0:a.maintenance)!=null&&E.enabled),j=!!((N=a==null?void 0:a.quietTime)!=null&&N.active),b=x.useRef(null),k=((B=Array.from(n.trim())[0])==null?void 0:B.toLocaleUpperCase("en-NZ"))||"A",p=a&&o&&!h&&!j?"ok":a||!i?"bad":"checking",y=async()=>{if(!(g||!a)){v(!0);try{await d(!h)}finally{v(!1)}}};return x.useEffect(()=>r(!1),[f.pathname]),x.useEffect(()=>{if(!c)return;const S=K=>{var ne;(ne=b.current)!=null&&ne.contains(K.target)||l(!1)},L=K=>{K.key==="Escape"&&l(!1)};return document.addEventListener("mousedown",S),window.addEventListener("keydown",L),()=>{document.removeEventListener("mousedown",S),window.removeEventListener("keydown",L)}},[c]),x.useEffect(()=>{if(!t)return;const S=document.body.style.overflow;document.body.style.overflow="hidden";const L=K=>{K.key==="Escape"&&r(!1)};return window.addEventListener("keydown",L),()=>{document.body.style.overflow=S,window.removeEventListener("keydown",L)}},[t]),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":t,"aria-controls":"rail",onClick:()=>r(S=>!S),children:e.jsx(ee,{name:"menu"})}),e.jsx("div",{className:"topbar-spacer"}),e.jsxs("div",{className:"topbar-tools",children:[e.jsx(Qs,{}),e.jsxs("span",{className:"topbar-version",children:["gateway ",s||"unknown"]}),e.jsx("button",{type:"button",className:"topbar-status","data-tone":p,"aria-pressed":h,disabled:!a||!o||g||j,title:a?j?"Memby quiet time is active":h?"Bring Memby back online":o?"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":a&&o?"Memby is online. Take it offline":i?"Checking Memby status":"Memby is not responding",onClick:()=>h?void y():m(!0),children:e.jsx("span",{className:"dot","aria-hidden":"true"})}),e.jsx(sn,{}),e.jsxs("div",{className:"account-menu","data-open":c||void 0,ref:b,children:[e.jsxs("button",{type:"button",className:"account-trigger","aria-haspopup":"menu","aria-expanded":c,"aria-label":`Signed in as ${n}`,onClick:()=>l(S=>!S),children:[e.jsx("span",{className:"account-avatar","aria-hidden":"true",children:k}),e.jsx("span",{className:"account-name",children:n}),e.jsx(ee,{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:k}),e.jsxs("span",{children:[e.jsx("small",{children:"Signed in as"}),e.jsx("b",{children:n})]})]}),e.jsxs(ps,{to:"/admin/settings",role:"menuitem",onClick:()=>l(!1),children:[e.jsx(ee,{name:"sliders"}),"Gateway settings"]}),e.jsx("form",{method:"post",action:"/admin/logout",children:e.jsxs("button",{type:"submit",role:"menuitem",children:[e.jsx(ee,{name:"logout"}),"Log out"]})})]}):null]})]})]}),t?e.jsx("button",{type:"button",className:"rail-scrim","aria-label":"Close sections",onClick:()=>r(!1)}):null,e.jsx(tn,{open:t,onNavigate:()=>r(!1)}),e.jsx("main",{className:"page",id:"main",children:e.jsx(Ts,{})}),u?e.jsx(he,{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:g,onConfirm:()=>{m(!1),y()},onCancel:()=>m(!1)}):null]})}const Es=x.createContext(null),rn=5e3;function ln({children:s}){const[n,o]=x.useState([]),i=x.useRef(1),a=x.useCallback(c=>{o(l=>l.filter(g=>g.id!==c))},[]),d=x.useCallback((c,l="ok")=>{const g=i.current++;o(v=>[...v,{id:g,message:c,tone:l}]),window.setTimeout(()=>a(g),rn)},[a]),t=x.useCallback(async(c,l)=>{try{const g=await c();return l&&d(l,"ok"),g}catch(g){d(g instanceof Error?g.message:String(g),"bad");return}},[d]),r=x.useMemo(()=>({show:d,wrap:t}),[d,t]);return e.jsxs(Es.Provider,{value:r,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(ee,{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(ee,{name:"close"})})]},c.id))})]})}function Y(){const s=x.useContext(Es);if(!s)throw new Error("useToast used outside ToastProvider");return s}function J(s,n={}){const{pollMs:o,enabled:i=!0}=n,[a,d]=x.useState(),[t,r]=x.useState(""),[c,l]=x.useState(i),[g,v]=x.useState(!1),u=x.useRef(0),m=x.useRef(!1),f=x.useCallback(async()=>{if(!i)return;const h=++u.current;m.current&&v(!0);try{const j=await q.get(s);if(h!==u.current)return;d(j),r(""),m.current=!0}catch(j){if(h!==u.current)return;r(j instanceof Error?j.message:String(j))}finally{h===u.current&&(l(!1),v(!1))}},[s,i]);return x.useEffect(()=>(m.current=!1,l(!0),f(),()=>{u.current+=1}),[f]),x.useEffect(()=>{if(!o||!i)return;let h;const j=()=>{window.clearInterval(h),h=document.hidden?void 0:window.setInterval(()=>void f(),o)},b=()=>{j(),document.hidden||f()};return j(),document.addEventListener("visibilitychange",b),()=>{window.clearInterval(h),document.removeEventListener("visibilitychange",b)}},[o,i,f]),{data:a,error:t,loading:c,refreshing:g,reload:f,set:d}}function Q(){const[s,n]=x.useState(null),o=x.useRef(!0);x.useEffect(()=>()=>{o.current=!1},[]);const i=x.useCallback(async(a,d)=>{n(a);try{return await d(),!0}finally{o.current&&n(null)}},[]);return{busy:s,run:i}}function on(){var j,b,k,p,y,E;const{status:s,error:n,loading:o}=ae(),i=J("/admin/api/runtime",{pollMs:3e4}),a=J("/admin/api/views",{pollMs:6e4});if(o||!s)return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Overview",intro:"What the gateway is doing right now."}),e.jsx(U,{message:n}),e.jsx(W,{})]});const d=s.features??{features:[],revision:0,safeMode:!1},t=d.features??[],r=s.clients??[],c=r.filter(N=>Ne(N.lastSeen)).length,l=s.updatePolicy??{},g=!!l.minimumVersion&&l.minimumVersion===l.latestVersion,v=s.playbackPolicy,u=s.mdblist,m=s.forYou,f=(s.runs??[]).slice(0,5),h=i.data;return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Overview",intro:"What the gateway is doing right now."}),e.jsx(U,{message:n}),e.jsx(re,{tiles:[{label:"items in the library",value:w(s.library.total),icon:"library",tone:"data"},{label:"people signed in",value:w((s.requestUsers??[]).length),icon:"people",tone:"note"},{label:`devices · ${c} active now`,value:w(r.length),icon:"tv",tone:"info"},{label:`visits today · ${((j=a.data)==null?void 0:j.lastWeek.visits)??0} this time last week`,value:w((b=a.data)==null?void 0:b.today.visits),icon:"overview",tone:"data"},{label:`viewers today · ${((k=a.data)==null?void 0:k.lastWeek.viewers)??0} this time last week`,value:w((p=a.data)==null?void 0:p.today.viewers),icon:"people",tone:"note"},{label:"optional features on",value:`${t.filter(N=>N.enabled).length} / ${t.length}`,icon:"sliders",tone:"ok"},{label:"last import",value:P(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsxs(oe,{cols:"2",children:[e.jsx(R,{title:"What televisions are being told",intro:"The answers the gateway is giving every set right now.",icon:"tv",tone:"info",children:e.jsx(Oe,{rows:[{label:"Availability",value:(y=s.maintenance)!=null&&y.enabled?e.jsx(C,{tone:"bad",children:"offline for maintenance"}):(E=s.quietTime)!=null&&E.active?e.jsx(C,{tone:"warn",children:"quiet time active"}):e.jsx(C,{tone:"ok",children:"online"})},{label:"Feature control plane",value:d.safeMode?e.jsx(C,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(C,{tone:"ok",children:["revision r",w(d.revision)]})},{label:"App update prompt",value:l.enabled?e.jsxs(C,{tone:g?"warn":"ok",children:[g?"required · ":"optional · ",l.latestVersion]}):e.jsx(C,{children:"off"})},{label:"Catalogue import",value:s.syncRunning?e.jsx(C,{tone:"warn",children:"running"}):e.jsxs(C,{children:["every ",s.syncEvery]})},{label:"Playback preroll",value:(v==null?void 0:v.prerollEnabled)===!1?e.jsx(C,{children:"off"}):e.jsxs(C,{tone:"ok",children:[((v==null?void 0:v.prerollDurationMs)??6500)/1e3,"s"]})}]})}),e.jsx(R,{title:"Services",intro:"The services this gateway leans on, and whether they answered.",icon:"wrench",tone:"note",children:e.jsx(Oe,{rows:[{label:"Movies (Radarr)",value:s.radarrReady?e.jsx(C,{tone:"ok",children:"ready"}):e.jsx(C,{children:"not configured"})},{label:"Series (Sonarr)",value:s.sonarrReady?e.jsx(C,{tone:"ok",children:"ready"}):e.jsx(C,{children:"not configured"})},{label:"MDBList ratings",value:u!=null&&u.enabled?e.jsxs(C,{tone:"ok",children:[w(u.cachedTitles)," titles stored"]}):e.jsx(C,{children:u!=null&&u.apiKeyConfigured?"off · key saved":"off · no key"})},{label:"For You pools",value:s.forYouRunning?e.jsx(C,{tone:"warn",children:"rebuilding"}):e.jsxs(C,{children:[w((m==null?void 0:m.candidates)??0)," ranked candidates"]})},{label:"Recommendation profiles",value:e.jsx("span",{className:"mono",children:w((m==null?void 0:m.profiles)??0)})}]})})]}),e.jsxs(oe,{cols:"2",children:[e.jsx(R,{title:"Latest imports",intro:"The last few catalogue synchronisations.",icon:"sync",tone:"data",actions:e.jsx(ie,{to:"/admin/imports",children:"All imports"}),children:e.jsx(G,{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(se,{columns:4,children:"No imports have run yet."}):f.map(N=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(N.startedAt)}),e.jsx("td",{children:N.kind}),e.jsx("td",{children:e.jsx(C,{tone:N.status==="success"?"ok":N.status==="running"?"warn":"bad",children:N.status})}),e.jsx("td",{className:"num",children:w(N.itemsUpserted)})]},N.id||N.startedAt))})]})})}),e.jsx(R,{title:"Process",intro:"The container the gateway is served from.",icon:"chip",tone:"info",children:h?e.jsxs(e.Fragment,{children:[e.jsx(Ms,{tiles:[{label:"goroutines",value:w(h.goroutines)},{label:"heap in use",value:ye(h.heapInuse)},{label:"reserved",value:ye(h.sys)},{label:"collections",value:w(h.numGc)}]}),e.jsxs("p",{className:"hint",children:["Next collection at ",ye(h.nextGc)," · memory limit"," ",h.memoryLimit>0&&h.memoryLimit`/admin/api/notifications${Se({days:a,type:t,severity:c,unread:g,limit:f,offset:u*f})}`,[a,t,c,g,u]),{data:j,error:b,loading:k,reload:p}=J(h),y=async()=>{await o(),await p()};return e.jsxs(e.Fragment,{children:[e.jsx(O,{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(A,{onClick:()=>void y(),icon:"check",children:"Mark all read"}):void 0}),e.jsx(U,{message:b}),e.jsx(re,{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(D,{label:"Window",children:e.jsx(Ie,{value:a,options:cn.map(E=>({value:E.value,label:E.label})),onChange:E=>{d(E),m(0)}})}),e.jsx(D,{label:"Kind",children:e.jsxs("select",{value:t,onChange:E=>{r(E.target.value),m(0)},children:[e.jsx("option",{value:"",children:"Everything"}),((j==null?void 0:j.types)??[]).map(E=>e.jsxs("option",{value:E.type,children:[Ze(E.type)," (",E.count,")"]},E.type))]})}),e.jsx(D,{label:"Severity",children:e.jsxs("select",{value:c,onChange:E=>{l(E.target.value),m(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:g?"unread":"",onChange:E=>{v(E.target.value==="unread"),m(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(A,{variant:"quiet",size:"sm",icon:"refresh",onClick:()=>{p(),i()},children:"Refresh"})})]}),k?e.jsx(W,{}):e.jsx(R,{title:"Events",icon:"bell",tone:"info",footer:((j==null?void 0:j.total)??0)>f?e.jsxs(e.Fragment,{children:[e.jsx(A,{size:"sm",disabled:u===0,onClick:()=>m(u-1),children:"Newer"}),e.jsx(A,{size:"sm",disabled:(u+1)*f>=((j==null?void 0:j.total)??0),onClick:()=>m(u+1),children:"Older"})]}):void 0,children:e.jsx(G,{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(se,{columns:6,children:"Nothing has happened in this window."}):j==null?void 0:j.events.map(E=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(E.occurredAt),children:pe(E.occurredAt)}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(Le,{name:Ss(E.type),tone:Ns(E)}),Ze(E.type)]})}),e.jsxs("td",{children:[e.jsx("b",{children:E.title}),E.summary?e.jsx("div",{className:"muted",children:E.summary}):null]}),e.jsx("td",{className:"muted nowrap",children:E.actor||"—"}),e.jsx("td",{className:"muted nowrap",children:E.target||"—"}),e.jsxs("td",{className:"nowrap",children:[E.readAt?null:e.jsx(C,{tone:"ok",children:"new"}),E.link?e.jsx(ie,{className:"table-row-link",to:E.link,children:"Open"}):null]})]},E.id))})]})})})]})}function hn(){const{data:s,error:n,loading:o}=J("/admin/api/accounts",{pollMs:6e4}),i=(s==null?void 0:s.accounts)??[],a=i.flatMap(r=>r.devices??[]),d=i.filter(r=>{var c;return(c=r.recommendations)==null?void 0:c.completed}).length,t=i.filter(r=>{var c,l;return((c=r.recommendations)==null?void 0:c.prompted)&&!((l=r.recommendations)!=null&&l.completed)}).length;return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Memby users",intro:"Who uses Memby, and the devices they are signed in on."}),e.jsx(U,{message:n}),e.jsx(ue,{tone:"info",children:"This is the Memby user list, not the Emby user directory. A person appears here only after signing in to the Memby app. Removing access signs their Memby devices out and does not delete or change their Emby account."}),o?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(re,{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 quarter hour",value:w(a.filter(r=>Ne(r.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"recommendation setups completed",value:w(d),icon:"check",tone:"ok"},{label:"setup prompts queued",value:w(t),icon:"sparkle",tone:"note"}]}),e.jsx("section",{className:"card flush",children:i.length===0?e.jsx(Z,{children:"No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here."}):i.map(r=>{var u,m;const c=r.devices??[],l=c.filter(f=>Ne(f.lastSeen)).length,g=(u=r.recommendations)!=null&&u.completed?{label:"personalised",tone:"ok"}:(m=r.recommendations)!=null&&m.prompted?{label:"prompt queued",tone:"warn"}:{label:"not invited",tone:void 0},v=_e(r.lastSeen);return e.jsxs(ie,{className:"list-row",to:`/admin/accounts/${encodeURIComponent(r.id)}`,children:[e.jsxs("span",{className:"list-main",children:[e.jsx("span",{className:"avatar",children:r.initials||ys(r.username)}),e.jsxs("span",{children:[e.jsxs("span",{className:"list-title",children:[r.username||"Unnamed user",e.jsx("span",{className:"dot-state","data-tone":v.tone,title:v.label})]}),e.jsxs("span",{className:"list-meta",children:[w(c.length)," device",c.length===1?"":"s",l?` · ${l} active now`:""," · last seen ",P(r.lastSeen)]})]})]}),e.jsxs("span",{className:"list-actions",children:[e.jsx(C,{tone:g.tone,children:g.label}),e.jsx("span",{className:"crumb",children:"Manage"})]})]},r.id)})})]})]})}function De(s){const n=String(s??"").replace("#","");return n.length!==8?`#${n}`:`#${n.slice(2)}${n.slice(0,2)}`}function un(){const{userId:s=""}=$e(),n=Ve(),{wrap:o}=Y(),{busy:i,run:a}=Q(),d=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:t,error:r,loading:c,reload:l}=J("/admin/api/accounts",{pollMs:3e4}),[g,v]=x.useState(null),[u,m]=x.useState(null),[f,h]=x.useState(null),[j,b]=x.useState(null),[k,p]=x.useState(null),y=((t==null?void 0:t.accounts)??[]).find($=>$.id===s),E=(t==null?void 0:t.catalogue)??[],N=(t==null?void 0:t.themes)??[];x.useEffect(()=>{var $;g===null&&y&&v({...(($=y.settings)==null?void 0:$.preferences)??{}})},[y,g]),x.useEffect(()=>{f===null&&y&&h({...y.notifications})},[y,f]),x.useEffect(()=>{if(u!==null||!y)return;const $=y.themes??[];m($.length===0?N.map(I=>I.id):$)},[y,u,N]);const B=x.useMemo(()=>{const $=[];for(const I of E){let H=$.find(M=>M.name===I.area);H||$.push(H={name:I.area,definitions:[]}),H.definitions.push(I)}return $},[E]),S=($,I,H,M)=>a($,async()=>{const F=await o(I,H);b(null),F!==void 0&&(M==null||M()),await l()});if(c)return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"User",crumbs:e.jsx(ie,{to:"/admin/accounts",children:"← All users"})}),e.jsx(W,{})]});if(!y)return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"User",crumbs:e.jsx(ie,{to:"/admin/accounts",children:"← All users"})}),e.jsx(U,{message:r}),e.jsx(R,{children:e.jsx(Z,{children:"This user is no longer signed in to Memby."})})]});const L=y.devices??[],K=L.filter($=>Ne($.lastSeen)).length,ne=y.settings??{},X=y.recommendations??{};return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:y.username||"Unnamed user",intro:`Memby user · ${w(L.length)} device${L.length===1?"":"s"} · last seen ${P(y.lastSeen)}`,crumbs:e.jsx(ie,{to:"/admin/accounts",children:"← All users"}),actions:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"avatar",children:y.initials||ys(y.username)}),K?e.jsxs(C,{tone:"ok",children:[K," active now"]}):e.jsx(C,{children:"idle"}),e.jsx(te,{children:y.id})]})}),e.jsx(U,{message:r}),e.jsxs(oe,{cols:"wide",children:[e.jsx(R,{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:L.length===0?e.jsx(Z,{children:"No devices are signed in to this user."}):e.jsx("div",{className:"list",children:L.map($=>{const I=_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":I.tone,title:I.label})," ",e.jsx(ie,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent($.id)}`,children:$.name||"Memby TV"})]}),e.jsxs("p",{children:[$.version?`Memby ${$.version}`:"Legacy Memby client"," · ",I.label," · last seen ",P($.lastSeen)," · signed in ",P($.signedInAt)]}),($.versions??[]).length>0?e.jsx("div",{className:"chips",children:($.versions??[]).map(H=>e.jsxs(te,{tone:H.version===$.version?"ok":void 0,children:[H.version,H.version===$.version?" · now":""]},H.version))}):null]}),e.jsxs("div",{className:"list-actions",children:[e.jsx(A,{size:"sm",disabled:!$.id,onClick:()=>p({id:$.id,name:$.name}),children:"Rename"}),e.jsx(A,{size:"sm",variant:"danger",disabled:!$.id,onClick:()=>b({kind:"remove-device",deviceId:$.id,name:$.name}),children:"Sign out"})]})]},$.id||$.name)})})}),e.jsxs(R,{title:"Recommendation setup",intro:"The prompt appears the next time this person opens Memby on any of their televisions.",icon:"sparkle",tone:"note",footer:X.completed?e.jsx(A,{busy:i==="reset-rec",onClick:()=>b({kind:"reset-recommendations"}),children:"Clear stored choices"}):X.prompted?e.jsx(A,{onClick:()=>b({kind:"cancel-prompt"}),children:"Cancel prompt"}):e.jsx(A,{variant:"primary",busy:i==="prompt",onClick:()=>void S("prompt",()=>q.put(`${d}/recommendations/prompt`),"Setup prompt queued."),children:"Send setup prompt"}),children:[e.jsx("div",{className:"row tight",children:X.completed?e.jsx(C,{tone:"ok",children:"completed"}):X.prompted?e.jsx(C,{tone:"warn",children:"prompt queued"}):e.jsx(C,{children:"not invited"})}),e.jsx(mn,{prompt:X})]})]}),e.jsx(R,{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(C,{tone:"ok",children:"enabled"}):e.jsx(C,{children:"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"primary",busy:i==="notifications",onClick:()=>void S("notifications",()=>q.put(`${d}/notifications`,f),"Notification settings saved.",()=>h(null)),children:"Save notifications"}),e.jsx(A,{onClick:()=>{h(null),l()},children:"Discard changes"})]}),children:f?e.jsxs("div",{className:"checks columns",children:[e.jsx(z,{label:"All notifications",hint:"The master switch. Turning this off hides every optional notification below.",checked:f.enabled,onChange:$=>h(I=>I&&{...I,enabled:$})}),e.jsx(z,{label:"My Shows return dates",hint:"Remind this person when a followed show is about to return.",checked:f.showReturnAlerts,disabled:!f.enabled,onChange:$=>h(I=>I&&{...I,showReturnAlerts:$})}),e.jsx(z,{label:"Sonarr television alerts",hint:"New episodes, additions and cancellation news supplied by Sonarr.",checked:f.sonarrAlerts,disabled:!f.enabled,onChange:$=>h(I=>I&&{...I,sonarrAlerts:$})}),e.jsx(z,{label:"Radarr film alerts",hint:"Notify this person when Radarr imports a new film.",checked:f.radarrAlerts,disabled:!f.enabled,onChange:$=>h(I=>I&&{...I,radarrAlerts:$})}),e.jsx(z,{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:$=>h(I=>I&&{...I,updateAlerts:$})}),e.jsx(z,{label:"Library activity",hint:"Show alerts after the Memby library catalogue is refreshed.",checked:f.libraryAlerts,disabled:!f.enabled,onChange:$=>h(I=>I&&{...I,libraryAlerts:$})}),e.jsx(z,{label:"Service status",hint:"Memby deployment and Emby outage or recovery notices. Maintenance mode itself still applies.",checked:f.systemAlerts,disabled:!f.enabled,onChange:$=>h(I=>I&&{...I,systemAlerts:$})})]}):null}),e.jsx(R,{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:ne.saved?e.jsxs(C,{tone:ne.source==="admin"?"warn":"ok",children:["r",w(ne.revision)," · ",ne.source||"device"," · ",P(ne.updatedAt)]}):e.jsx(C,{children:"defaults · never synced"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"primary",busy:i==="push",onClick:()=>void S("push",()=>q.put(`${d}/preferences`,{preferences:g??{}}),"Pushed to their televisions.",()=>v(null)),children:"Push to their televisions"}),e.jsx(A,{onClick:()=>{v(null),l()},children:"Discard changes"}),e.jsx(A,{onClick:()=>b({kind:"reset-preferences"}),children:"Restore defaults"}),e.jsx(ie,{className:"crumb",to:`/admin/accounts/${encodeURIComponent(s)}/settings`,children:"History and rollback →"})]}),children:B.map($=>e.jsxs("div",{className:"group",children:[e.jsx("p",{className:"group-label",children:$.name}),$.definitions.map(I=>e.jsx(pn,{definition:I,value:g==null?void 0:g[I.key],onChange:H=>v(M=>({...M??{},[I.key]:H}))},I.key))]},$.name))}),e.jsx(R,{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:(y.themes??[]).length===0?e.jsx(C,{children:"all schemes"}):e.jsxs(C,{tone:"note",children:[w((y.themes??[]).length)," of ",w(N.length)]}),footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"primary",busy:i==="themes",onClick:()=>(u??[]).length===0?b({kind:"no-themes"}):void S("themes",()=>q.put(`${d}/themes`,{themes:u??[]}),"Colour schemes saved.",()=>m(null)),children:"Save colour schemes"}),e.jsx(A,{onClick:()=>m(N.map($=>$.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:N.map($=>{var I,H,M;return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:(u??[]).includes($.id),onChange:F=>m(T=>F.target.checked?[...T??[],$.id]:(T??[]).filter(_=>_!==$.id))}),e.jsx("span",{className:"switch"}),e.jsx("span",{className:"swatch",style:{"--swatch-surface":De((I=$.palette)==null?void 0:I.surface),"--swatch-accent":De((H=$.palette)==null?void 0:H.accent),"--swatch-hairline":De((M=$.palette)==null?void 0:M.hairline)},children:e.jsx("i",{})}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:$.name}),e.jsx("p",{children:$.description})]})]},$.id)})})}),e.jsx(R,{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(A,{variant:"danger",onClick:()=>b({kind:"remove-account"}),children:"Remove Memby access"})}),k?e.jsx(xn,{initial:k.name,busy:i==="rename",onCancel:()=>p(null),onConfirm:$=>void S("rename",()=>q.put(`${d}/devices/${encodeURIComponent(k.id)}`,{deviceName:$}),"Device renamed.",()=>p(null))}):null,j?e.jsx(jn,{pending:j,busy:i,username:y.username,onCancel:()=>b(null),onConfirm:()=>{switch(j.kind){case"remove-device":return void S("remove-device",()=>q.del(`${d}/devices/${encodeURIComponent(j.deviceId)}`),"Device signed out.");case"remove-account":return void S("remove-account",()=>q.del(`${d}/sessions`),"Memby access removed.",()=>n("/admin/accounts"));case"reset-recommendations":case"cancel-prompt":return void S("reset-rec",()=>q.del(`${d}/recommendations`),"Recommendation choices cleared.");case"reset-preferences":return void S("reset-prefs",()=>q.del(`${d}/preferences`),"Defaults restored.",()=>v(null));case"no-themes":return void S("themes",()=>q.put(`${d}/themes`,{themes:[]}),"Colour schemes saved.",()=>m(null))}}}):null]})}function mn({prompt:s}){const n=s.ratings??[],o=[["Genres",s.genres],["Studios",s.studios],["Actors",s.actors],["Actresses",s.actresses],["Directors",s.directors],["Types",s.contentTypes]],i=[...n.map(a=>e.jsxs(te,{tone:"warn",children:[a.title," · ",w(a.rating)," ★"]},`r:${a.title}`)),...o.flatMap(([a,d])=>(d??[]).map(t=>e.jsxs(te,{children:[a,": ",t]},`${a}:${t}`)))];return i.length===0?e.jsx(Z,{children:"No recommendation selections have been saved."}):e.jsx("div",{className:"chips",children:i})}function pn({definition:s,value:n,onChange:o}){if(s.kind==="toggle")return e.jsx(z,{label:s.name,hint:s.description,checked:!!n,onChange:o});if(s.kind==="choice"||s.kind==="number"){const a=s.unit??"",d=s.kind==="number"?(s.numbers??[]).map(t=>({value:String(t),label:t===0?"No limit":a?`${t} ${a}`:String(t)})):s.options??[];return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("select",{value:String(n??""),onChange:t=>o(s.kind==="number"?Number(t.target.value):t.target.value),children:d.map(t=>e.jsx("option",{value:t.value,children:t.label},t.value))})})}if(s.kind==="multi"){const a=Array.isArray(n)?n:[],d=[...a,...(s.options??[]).map(t=>t.value).filter(t=>!a.includes(t))];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:d.map(t=>{const r=(s.options??[]).find(c=>c.value===t);return r?e.jsx(z,{label:r.label,checked:a.includes(t),onChange:c=>o(c?[...a,t]:a.filter(l=>l!==t))},t):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:"Generated from their name",onChange:a=>o(a.target.value.toLocaleUpperCase("en-NZ"))})});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=>o(a.target.value.split(` -`).map(d=>d.trim()).filter(Boolean))})})}function xn({initial:s,busy:n,onConfirm:o,onCancel:i}){const[a,d]=x.useState(s||"Memby TV");return e.jsx("div",{className:"scrim",onPointerDown:t=>t.target===t.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:t=>d(t.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(A,{variant:"quiet",onClick:i,children:"Cancel"}),e.jsx(A,{variant:"primary",busy:n,disabled:!a.trim(),onClick:()=>o(a.trim()),children:"Rename"})]})]})})}function jn({pending:s,busy:n,username:o,onConfirm:i,onCancel:a}){const t={"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 ${o||"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},"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(he,{title:t.title,body:t.body,confirmLabel:t.label,destructive:t.destructive,busy:!!n,onConfirm:i,onCancel:a})}function vn(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 o=Array.isArray(n)?n:[];return o.length===0?"None":o.map(i=>{var a;return((a=(s.options??[]).find(d=>d.value===i))==null?void 0:a.label)??i}).join(", ")}function bn(){const{userId:s=""}=$e(),{wrap:n}=Y(),{busy:o,run:i}=Q(),a=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:d,error:t,loading:r,reload:c}=J(`${a}/preferences/history`,{pollMs:3e4}),[l,g]=x.useState(new Set),[v,u]=x.useState(null),m=(d==null?void 0:d.username)||"this account",f=(d==null?void 0:d.devices)??[],h=(d==null?void 0:d.revisions)??[],j=(d==null?void 0:d.catalogue)??[],b=p=>g(y=>{const E=new Set(y);return E.has(p)?E.delete(p):E.add(p),E}),k=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(O,{title:"Settings history",intro:`Every change to ${m}'s synced settings, and which of their televisions has taken it.`,crumbs:e.jsxs(ie,{to:`/admin/accounts/${encodeURIComponent(s)}`,children:["← ",m]})}),e.jsx(U,{message:t}),r?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(R,{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:d!=null&&d.saved?e.jsxs(C,{tone:d.currentSource==="admin"?"warn":"ok",children:["now on r",w(d.currentRevision)," · ",d.currentSource||"device"]}):e.jsx(C,{children:"defaults · never synced"}),children:f.length===0?e.jsx(Z,{children:"No television has been signed in to this account."}):e.jsx("div",{className:"list",children:f.map(p=>{const y=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":y})," ",p.name||"Memby TV"," ",p.signedOut?e.jsx(C,{children:"signed out"}):null]}),e.jsxs("p",{children:[p.never?"Has not fetched these settings yet":`Holding r${w(p.revision)} · taken ${P(p.ackedAt)}`,p.clientVersion?` · Memby ${p.clientVersion}`:"",p.signedOut?"":` · last seen ${P(p.lastSeen)}`]})]}),e.jsx("div",{className:"list-actions",children:p.never?e.jsx(C,{children:"never taken one"}):p.behind?e.jsxs(C,{tone:"bad",children:[w(p.behind)," behind"]}):e.jsx(C,{tone:"ok",children:"up to date"})})]},p.deviceId||p.name)})})}),e.jsx(R,{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(G,{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(se,{columns:6,children:"Nothing has been changed on this account yet."}):h.flatMap(p=>{const y=l.has(p.revision),E=p.acks??[],N=p.changes??[],B=[e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(p.createdAt)}),e.jsxs("td",{className:"num nowrap",children:["r",w(p.revision)," ",p.current?e.jsx(C,{tone:"ok",children:"current"}):null]}),e.jsxs("td",{className:"nowrap",children:[e.jsx(C,{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"}):N.length===0?e.jsx("span",{className:"muted",children:"No visible change"}):e.jsx("div",{className:"chips",children:N.map((S,L)=>e.jsxs(te,{children:[S.name,": ",S.before," → ",S.after]},`${S.name}:${L}`))})}),e.jsx("td",{className:"num",children:E.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{title:E.map(S=>S.deviceName||S.deviceId).join(", "),children:w(E.length)})}),e.jsx("td",{className:"num nowrap",children:e.jsxs("span",{className:"list-actions",children:[e.jsx(A,{size:"sm",onClick:()=>b(p.revision),children:y?"Hide":"Show"}),p.current?null:e.jsx(A,{size:"sm",onClick:()=>u(p.revision),children:"Restore"})]})})]},p.revision)];return y&&B.push(e.jsx("tr",{children:e.jsx("td",{colSpan:6,className:"muted",children:e.jsx("div",{className:"chips",children:j.map(S=>{var L;return e.jsxs(te,{children:[S.name,":"," ",vn(S,(L=p.preferences)==null?void 0:L[S.key])]},S.key)})})})},`${p.revision}:detail`)),B})})]})})})]}),v!==null?e.jsx(he,{title:`Restore revision ${v}?`,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:o==="restore",onConfirm:()=>void k(v),onCancel:()=>u(null)}):null]})}function gn({client:s}){const n=s.versions??[];return n.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{className:"versions",children:n.map(o=>e.jsx(te,{tone:o.version===s.version?"ok":void 0,children:o.version},o.version))})}function fn(){const{status:s,error:n,loading:o}=ae(),i=(s==null?void 0:s.clients)??[],a=i.filter(t=>(t.capabilities??[]).includes("server_features_v1")),d=new Set(i.map(t=>t.version).filter(Boolean));return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands."}),e.jsx(U,{message:n}),o?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"devices known",value:w(i.length),icon:"tv",tone:"info"},{label:"active in the last quarter hour",value:w(i.filter(t=>Ne(t.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(d.size),icon:"download",tone:"note"}]}),e.jsx(R,{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(G,{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(se,{columns:6,children:"No devices have signed in yet."}):i.map(t=>{const r=(t.capabilities??[]).includes("server_features_v1"),c=_e(t.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(ie,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(t.deviceId)}`,children:t.deviceName||"Memby TV"})]})}),e.jsx("td",{className:"muted",children:t.username}),e.jsx("td",{className:"mono",children:t.version||"legacy"}),e.jsx("td",{children:e.jsx(gn,{client:t})}),e.jsx("td",{children:e.jsx(C,{tone:r?"ok":"warn",children:r?"reported":"missing"})}),e.jsx("td",{className:"nowrap muted",children:P(t.lastSeen)})]},`${t.deviceId}:${t.username}`)})})]})})})]})]})}const Qe={user:"",q:"",ip:"",outcome:"",from:"",to:""},yn=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function wn(){var p,y,E,N,B,S;const[s,n]=x.useState("log"),[o,i]=x.useState(7),[a,d]=x.useState(Qe),[t,r]=x.useState(0),c=100,l=x.useMemo(()=>Se({...a,days:a.from?void 0:o||void 0,limit:c,offset:t*c}),[a,o,t]),g=J(`/admin/api/logins${l}`,{enabled:s==="log"}),v=J(`/admin/api/logins/devices${l}`,{enabled:s==="devices"}),u=((p=g.data)==null?void 0:p.users)??((y=v.data)==null?void 0:y.users)??[],m=((E=g.data)==null?void 0:E.totals)??((N=v.data)==null?void 0:N.totals),f=((B=g.data)==null?void 0:B.retentionDays)??((S=v.data)==null?void 0:S.retentionDays)??90,h=s==="log"?g.loading:v.loading,j=s==="log"?g.error:v.error,b=L=>{d(K=>({...K,...L})),r(0)},k=Object.entries(a).some(([,L])=>L!=="")||!!a.from;return e.jsxs(e.Fragment,{children:[e.jsx(O,{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(Ie,{value:s,options:[{value:"log",label:"Log"},{value:"devices",label:"By device"}],onChange:n})}),e.jsx(U,{message:j}),m?e.jsx(re,{tiles:[{label:"Successful sign-ins",value:w(m.logins),icon:"key",tone:"ok"},{label:"Refused",value:w(m.failures),icon:"shield",tone:m.failures>0?"warn":void 0},{label:"Televisions",value:w(m.devices),icon:"tv",tone:"info"},{label:"People",value:w(m.users),icon:"people",tone:"note"},{label:"Addresses",value:w(m.addresses),icon:"globe",tone:"data"},{label:"History kept",value:`${f} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Ie,{value:a.from?-1:o,options:yn.map(L=>({value:L.value,label:L.label})),onChange:L=>{i(L),b({from:"",to:""})}})}),e.jsx(D,{label:"Person",children:e.jsxs("select",{value:a.user,onChange:L=>b({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=>b({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=>b({ip:L.target.value})})}),e.jsx(D,{label:"From",children:e.jsx("input",{type:"date",value:a.from,onChange:L=>b({from:L.target.value})})}),e.jsx(D,{label:"To",children:e.jsx("input",{type:"date",value:a.to,onChange:L=>b({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=>b({q:L.target.value})})}),e.jsx("div",{className:"filter-actions",children:k?e.jsx(A,{variant:"quiet",size:"sm",onClick:()=>{d(Qe),r(0)},children:"Clear"}):null})]}),h?e.jsx(W,{}):s==="log"?e.jsx(kn,{data:g.data,page:t,limit:c,onPage:r}):e.jsx(Nn,{data:v.data})]})}function kn({data:s,page:n,limit:o,onPage:i}){if(!s)return null;const a=s.events.length,d=s.total===0?0:n*o+1;return e.jsxs(e.Fragment,{children:[e.jsxs(oe,{cols:"wide",children:[e.jsx(R,{title:"Attempts per day",intro:"Grouped in the household's own timezone, so an evening sign-in stays on the day it happened.",icon:"chart",tone:"info",children:e.jsx(Cs,{data:s.days,labelOf:t=>t.day,valueOf:t=>t.logins+t.failures,toneOf:t=>t.failures>t.logins?"bad":void 0,title:t=>`${t.day}: ${t.logins} in, ${t.failures} refused, ${t.devices} televisions`})}),e.jsx(R,{title:"Where from",icon:"globe",tone:"data",children:s.addresses.length===0?e.jsx(Z,{children:"No addresses in this window."}):e.jsx("div",{className:"list",children:s.addresses.slice(0,8).map(t=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:t.ipAddress}),e.jsxs("p",{children:[w(t.logins)," in",t.failures>0?` · ${w(t.failures)} refused`:""]})]}),t.failures>0&&t.logins===0?e.jsx(C,{tone:"bad",children:"only refused"}):null]},t.ipAddress))})})]}),e.jsx(R,{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(d)}–${w(d+a-1)} of ${w(s.total)}`}),footer:s.total>o?e.jsxs(e.Fragment,{children:[e.jsx(A,{size:"sm",disabled:n===0,onClick:()=>i(n-1),children:"Newer"}),e.jsx(A,{size:"sm",disabled:(n+1)*o>=s.total,onClick:()=>i(n+1),children:"Older"})]}):void 0,children:e.jsx(G,{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(se,{columns:6,children:"No sign-in attempts match these filters."}):s.events.map(t=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(t.occurredAt)}),e.jsx("td",{children:t.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{children:t.deviceId?e.jsx(ie,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(t.deviceId)}`,children:t.deviceName||t.deviceId}):e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"mono nowrap",children:t.ipAddress||"—"}),e.jsx("td",{className:"mono",children:t.clientVersion||"—"}),e.jsx("td",{className:"nowrap",children:t.success?t.newDevice?e.jsx(C,{tone:"info",children:"first sign-in"}):e.jsx(C,{tone:"ok",children:"got in"}):e.jsx(C,{tone:"bad",children:t.failureReason||"refused"})})]},t.id))})]})})})]})}function Nn({data:s}){return s?e.jsx(R,{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(G,{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(se,{columns:9,children:"No television has connected in this window."}):s.devices.map(n=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(ie,{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?P(n.lastLogin):"—"}),e.jsx("td",{className:"mono",children:n.clientVersion||"—"})]},n.deviceId))})]})})}):null}const Sn=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Cn(){const{deviceId:s=""}=$e(),[n,o]=x.useState(7),i=x.useMemo(()=>`/admin/api/logins/devices/${encodeURIComponent(s)}${Se({days:n||void 0,limit:200})}`,[s,n]),{data:a,error:d,loading:t}=J(i,{enabled:!!s}),r=a==null?void 0:a.summary,c=(r==null?void 0:r.deviceName)||s;return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:c,intro:"One television's whole relationship with the gateway.",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(ie,{to:"/admin/clients",children:"Devices"}),e.jsx("span",{children:"/"}),e.jsx(ie,{to:"/admin/logins",children:"Sign-ins"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:c})]}),actions:e.jsx(Ie,{value:n,options:Sn.map(l=>({value:l.value,label:l.label})),onChange:o})}),e.jsx(U,{message:d}),t?e.jsx(W,{}):a?e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"Sign-ins today",value:w((r==null?void 0:r.loginsToday)??0),icon:"clock",tone:"ok"},{label:"Sign-ins in total",value:w((r==null?void 0:r.logins)??0),icon:"key",tone:"info"},{label:"Refused",value:w((r==null?void 0:r.failures)??0),icon:"shield",tone:((r==null?void 0:r.failures)??0)>0?"warn":void 0},{label:"Addresses seen",value:w((r==null?void 0:r.distinctIps)??0),icon:"globe",tone:"data"},{label:"First seen",value:r!=null&&r.firstLogin?P(r.firstLogin):"—",small:!0,icon:"history"},{label:"Last seen",value:r!=null&&r.lastLogin?P(r.lastLogin):"—",small:!0,icon:"pulse",tone:"note"}]}),e.jsxs(oe,{cols:"wide",children:[e.jsx(R,{title:"Connections per day",icon:"chart",tone:"info",children:e.jsx(Cs,{data:a.days,labelOf:l=>l.day,valueOf:l=>l.logins+l.failures,toneOf:l=>l.failures>l.logins?"bad":void 0,title:l=>`${l.day}: ${l.logins} in${l.failures?`, ${l.failures} refused`:""}`})}),e.jsxs("div",{className:"stack",children:[e.jsx(R,{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:(r==null?void 0:r.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:(r==null?void 0:r.clientVersion)||"unknown"})]})})]})}),e.jsx(R,{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(Z,{children:"No build history for this television."}):e.jsx("div",{className:"list",children:a.versions.map(l=>e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:l.version}),e.jsxs("p",{children:[P(l.firstSeen)," → ",P(l.lastSeen)]})]})},l.version))})})]})]}),e.jsx(R,{title:"Addresses",icon:"globe",tone:"data",children:a.addresses.length===0?e.jsx(Z,{children:"No addresses recorded in this window."}):e.jsx("div",{className:"chips",children:a.addresses.map(l=>e.jsxs(te,{tone:l.failures>0?"warn":"data",children:[l.ipAddress," · ",w(l.logins),l.failures>0?` (+${w(l.failures)} refused)`:""]},l.ipAddress))})}),e.jsx(R,{title:"Every attempt",icon:"key",tone:"ok",actions:e.jsxs("span",{className:"filter-summary",children:[w(a.total)," in this window"]}),children:e.jsx(G,{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(se,{columns:6,children:"This television has not connected in the selected window."}):a.events.map(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(l.occurredAt)}),e.jsx("td",{children:l.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{className:"mono nowrap",children:l.ipAddress||"—"}),e.jsx("td",{className:"mono",children:l.clientVersion||"—"}),e.jsx("td",{className:"muted",children:l.method}),e.jsx("td",{className:"nowrap",children:l.success?l.newDevice?e.jsx(C,{tone:"info",children:"first sign-in"}):e.jsx(C,{tone:"ok",children:"got in"}):e.jsx(C,{tone:"bad",children:l.failureReason||"refused"})})]},l.id))})]})})})]}):null]})}function Mn(){const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(!1),l=(s==null?void 0:s.library.byType)??{},g=!!(s!=null&&s.syncRunning),v=u=>t(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(O,{title:"Library",intro:"Import and inspect the catalogue Memby ranks."}),e.jsx(U,{message:n}),o||!s?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"items",value:w(s.library.total),icon:"library",tone:"data"},...Object.keys(l).sort().map(u=>({label:u,value:w(l[u]),icon:"list"})),{label:"last import",value:P(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsx(R,{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:g?"Import running…":`An incremental import runs automatically every ${s.syncEvery}.`}),children:e.jsxs("div",{className:"row",children:[e.jsx(A,{variant:"primary",icon:"sync",disabled:g,busy:d==="incremental",onClick:()=>void v("incremental"),children:"Sync new items"}),e.jsx(A,{icon:"database",disabled:g,onClick:()=>c(!0),children:"Full re-import"})]})})]}),r?e.jsx(he,{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:d==="full",onConfirm:()=>void v("full"),onCancel:()=>c(!1)}):null]})}const En={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 An(){const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(!1),[l,g]=x.useState(""),[v,u]=x.useState(!1),[m,f]=x.useState([]),[h,j]=x.useState(!1),b=s==null?void 0:s.mdblist;x.useEffect(()=>{h||!b||(c(b.enabled),f(b.sources??[]))},[b,h]);const k=()=>t("save",async()=>{await a(()=>q.post("/admin/api/mdblist-settings",{enabled:r,apiKey:l.trim(),clearApiKey:v,sources:m}),"Ratings settings saved."),g(""),u(!1),j(!1),await i()}),p=(b==null?void 0:b.cachedTitles)??0;return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Movie ratings",intro:"Optional MDBList scores on films and shows."}),e.jsx(U,{message:n}),o||!b?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"titles stored",value:w(p),icon:"database",tone:"data"},{label:"due to be re-checked",value:w(b.staleTitles),icon:"sync",tone:"warn"},{label:"sources shown",value:w((b.sources??[]).length),icon:"star",tone:"note"},{label:"API key",value:b.apiKeyConfigured?"saved":"not set",small:!0,icon:"key",tone:b.apiKeyConfigured?"ok":void 0}]}),e.jsxs(oe,{cols:"2",children:[e.jsxs(R,{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:r?e.jsxs(C,{tone:"ok",children:["on · ",m.length," sources"]}):e.jsx(C,{children:b.apiKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(z,{label:"Show external ratings on televisions",hint:"Off leaves the stored ratings in place.",checked:r,onChange:y=>{c(y),j(!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:l,placeholder:b.apiKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:y=>g(y.target.value)})}),e.jsx(z,{label:"Remove the saved key",checked:v,onChange:u})]}),e.jsx(R,{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:(b.availableSources??[]).length===0?e.jsx(Z,{children:"No rating sources are available."}):e.jsx("div",{className:"checks columns",children:(b.availableSources??[]).map(y=>e.jsx(z,{label:En[y]??y,checked:m.includes(y),onChange:E=>{j(!0),f(N=>E?[...N,y]:N.filter(B=>B!==y))}},y))})})]}),e.jsx(R,{children:e.jsxs("div",{className:"row",children:[e.jsx(A,{variant:"primary",busy:d==="save",onClick:()=>void k(),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."})]})})]})]})}function Rn(){var v;const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),r=(s==null?void 0:s.requestUsers)??[],c=((v=s==null?void 0:s.requestPolicy)==null?void 0:v.allowedUserIds)??[],l=x.useMemo(()=>new Map(((s==null?void 0:s.requestUsage)??[]).map(u=>[u.userId,u])),[s==null?void 0:s.requestUsage]),g=u=>t(`access-${u}`,async()=>{const m=c.includes(u)?c.filter(f=>f!==u):[...c,u];await a(()=>q.post("/admin/api/request-policy",{allowedUserIds:m}),m.includes(u)?"Request access granted.":"Request access removed."),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Media requests",intro:"Who can ask for something the library does not have."}),e.jsx(U,{message:n}),o?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(R,{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(C,{tone:s!=null&&s.radarrReady?"ok":"bad",children:["Movies ",s!=null&&s.radarrReady?"ready":"not configured"]}),e.jsxs(C,{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(Z,{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(R,{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:r.length===0?e.jsx(Z,{children:"No one has signed in yet."}):e.jsx(G,{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:r.map(u=>{const m=l.get(u.id),f=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:P(u.lastSeen)}),e.jsx("td",{className:"num",children:(m==null?void 0:m.requests)??0}),e.jsx("td",{className:"muted nowrap",children:m!=null&&m.lastRequest?P(m.lastRequest):"—"}),e.jsx("td",{children:e.jsx(A,{size:"sm",variant:f?"quiet":"primary",busy:d===`access-${u.id}`,onClick:()=>void g(u.id),children:f?"Remove access":"Give access"})})]},u.id)})})]})})})]})]})}function In(){const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(!1),l=s==null?void 0:s.forYou,g=!!(s!=null&&s.forYouRunning),v=(u,m,f)=>t(m,async()=>{await a(()=>q.post("/admin/api/for-you",{action:u}),f),c(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"For You",intro:"The prepared pools personalised rows are drawn from."}),e.jsx(U,{message:n}),o?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"Tracearr sessions",value:w((l==null?void 0:l.tracearrSessions)??0),icon:"play",tone:"info"},{label:"user profiles",value:w((l==null?void 0:l.profiles)??0),icon:"people",tone:"note"},{label:"ranked candidates",value:w((l==null?void 0:l.candidates)??0),icon:"sparkle",tone:"note"},{label:"last full import",value:P(l==null?void 0:l.lastFullImport),small:!0,icon:"clock"}]}),e.jsx(R,{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:g?"For You maintenance running…":"Prepared pools normally refresh in the background."}),children:e.jsxs("div",{className:"row",children:[e.jsx(A,{variant:"primary",icon:"download",disabled:g,busy:d==="import",onClick:()=>void v("incremental-import","import","Import started."),children:"Import recent sessions"}),e.jsx(A,{icon:"database",disabled:g,onClick:()=>c(!0),children:"Full Tracearr backfill"}),e.jsx(A,{icon:"sync",disabled:g,busy:d==="rebuild",onClick:()=>void v("rebuild-all","rebuild","Rebuild started."),children:"Rebuild all pools"})]})}),e.jsx(R,{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(ie,{to:"/admin/inspector",children:"Open the inspector"}),children:e.jsx(e.Fragment,{})})]}),r?e.jsx(he,{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:d==="full",onConfirm:()=>void v("full-import","full","Backfill started."),onCancel:()=>c(!1)}):null]})}const $n=[["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 Tn(s){return s?$n.flatMap(([n,o])=>Object.entries(s[o]??{}).map(([i,a])=>({dimension:n,name:i,weight:a.weight??0,evidence:a.evidence??0}))).sort((n,o)=>Math.abs(o.weight)-Math.abs(n.weight)):[]}const Ye=s=>`${s>=0?"+":""}${s.toFixed(3)}`;function Ln(){const{status:s}=ae(),{wrap:n}=Y(),{busy:o,run:i}=Q(),[a,d]=x.useState(""),[t,r]=x.useState("default"),[c,l]=x.useState("0"),[g,v]=x.useState(""),[u,m]=x.useState(null),[f,h]=x.useState("Choose a person to inspect their recommendations."),[j,b]=x.useState(""),k=(s==null?void 0:s.requestUsers)??[],p=()=>i("run",async()=>{if(!a){b("Choose a person to pressure-test.");return}b(""),h("Running the permission check and the scorer…");const S=new URLSearchParams({userId:a,context:t,minutes:c||"0",limit:"100"});g&&S.set("at",new Date(g).toISOString());const L=await n(()=>q.get(`/admin/api/recommendations?${S.toString()}`));L?(m(L),h(`Scored at ${new Date().toLocaleTimeString()}.`)):h("Pressure test failed.")}),y=Tn((u==null?void 0:u.profile)??null).slice(0,24),E=(u==null?void 0:u.actions)??[],N=(u==null?void 0:u.items)??[],B=(u==null?void 0:u.profileMeta)??{};return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Score inspector",intro:"Re-run the ranker for one person and read every component."}),e.jsx(U,{message:j}),e.jsx(R,{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(A,{variant:"primary",busy:o==="run",onClick:()=>void p(),children:"Run pressure test"}),e.jsx("span",{className:"hint",children:f})]}),children:e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Person",children:e.jsxs("select",{value:a,onChange:S=>d(S.target.value),children:[e.jsx("option",{value:"",children:"Choose a person…"}),k.map(S=>e.jsx("option",{value:S.id,children:S.username},S.id))]})}),e.jsx(D,{label:"Context",children:e.jsxs("select",{value:t,onChange:S=>r(S.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:S=>l(S.target.value)})}),e.jsx(D,{label:"Evaluate at",children:e.jsx("input",{type:"datetime-local",value:g,onChange:S=>v(S.target.value)})})]})}),u?e.jsxs(e.Fragment,{children:[e.jsx(re,{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(N.length),icon:"sparkle",tone:"note"},{label:"source events",value:w(B.sourceEvents??0),icon:"pulse",tone:"info"},{label:"algorithm",value:B.algorithmVersion||"—",small:!0,icon:"chip"},{label:"pool built",value:P(B.poolBuiltAt),small:!0,icon:"clock"}]}),e.jsxs(R,{title:"Profile evidence",intro:"The strongest learned affinities, and every explicit action this person has taken.",icon:"sparkle",tone:"note",children:[y.length===0?e.jsx(Z,{children:"No repeated affinity evidence yet; cold-start priors apply."}):e.jsx("div",{className:"chips",children:y.map(S=>e.jsxs(te,{tone:S.weight<0?"bad":void 0,children:[S.dimension,": ",S.name," ",Ye(S.weight)," · n=",w(S.evidence)]},`${S.dimension}:${S.name}`))}),E.length===0?e.jsx(Z,{children:"No explicit recommendation actions."}):e.jsx("div",{className:"chips",children:E.map((S,L)=>e.jsxs(te,{tone:"ok",children:[S.action,": ",S.title||S.itemId]},`${S.action}:${L}`))})]}),N.length===0?e.jsx(R,{children:e.jsx(Z,{children:"No candidates survived this context, the explicit exclusions and the permission filter."})}):e.jsx(oe,{children:N.map((S,L)=>{const K=S.explanation??{},ne=Object.entries(K.components??{}).sort((I,H)=>Math.abs(H[1])-Math.abs(I[1])),X=S.exposure??{},$=[S.type,S.year,S.runtimeMinutes?`${S.runtimeMinutes} min`:null,...S.genres??[]].filter(Boolean).join(" · ");return e.jsxs(R,{title:`#${L+1} · ${S.title}`,intro:$,actions:e.jsx(te,{tone:"ok",children:Number(K.total??0).toFixed(3)}),children:[e.jsxs("p",{className:"hint",children:[S.preparedReason||"No legacy prepared explanation",S.compatibilityLabel?` · ${S.compatibilityLabel}`:""]}),e.jsxs("div",{className:"chips",children:[(K.reasonCodes??[]).map(I=>e.jsx(te,{tone:"ok",children:I},I)),ne.map(([I,H])=>e.jsxs(te,{tone:H<0?"bad":void 0,children:[I,"=",Ye(H)]},I))]}),e.jsxs("details",{children:[e.jsx("summary",{className:"muted",children:"Pool, row and exposure detail"}),e.jsxs("p",{className:"hint",children:["Base rank ",w(S.baseRank??0)," · base ",Number(S.baseScore??0).toFixed(3)," · affinity ",Number(S.affinityScore??0).toFixed(3)," · compatibility"," ",Number(S.compatibilityScore??0).toFixed(3)," · impressions"," ",w(X.impressions??0)," · focuses ",w(X.focuses??0)," · selects"," ",w(X.selects??0)]}),e.jsx("div",{className:"chips",children:(S.eligibleRows??[]).map(I=>e.jsx(te,{tone:"ok",children:I},I))}),S.preparedEvidenceTitle?e.jsxs("p",{className:"hint",children:["Prepared evidence: ",S.preparedEvidenceTitle]}):null]})]},`${L}:${S.title}`)})})]}):null]})}const Dn=4,je=[{id:"home",label:"Home",type:"films and television shows"},{id:"movies",label:"Movies",type:"films"},{id:"tv_shows",label:"TV Shows",type:"television shows"}],fe=()=>({pinnedItems:[],primeSubtitle:""}),Ue=[{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 Xe(s){const n=s.getTimezoneOffset()*6e4;return new Date(s.getTime()-n).toISOString().slice(0,16)}function es(s){return!!(s&&Number.isFinite(new Date(s).getTime())&&new Date(s).getFullYear()>=2e3)}function qn(s){return s.frequency??"once"}function Fn(s){if(s.frequency==="daily")return`Every day · ${s.startTime}–${s.endTime}`;if(s.frequency==="weekly"){const n=Ue.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 ss(s,n){const o=new Date;o.setMinutes(Math.ceil(o.getMinutes()/30)*30,0,0);const i=new Date(o.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:o.toISOString(),endAt:i.toISOString(),priority:0,enabled:!0,placements:[a?n:"home"]}}function Pn(){var $,I,H;const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a,show:d}=Y(),{busy:t,run:r}=Q(),[c,l]=x.useState("home"),[g,v]=x.useState({home:fe(),movies:fe(),tv_shows:fe()}),[u,m]=x.useState(!1),[f,h]=x.useState(""),[j,b]=x.useState(null),[k,p]=x.useState([]),[y,E]=x.useState(null),N=s==null?void 0:s.heroPolicy;x.useEffect(()=>{var M,F,T;u||!N||(v({home:((M=N.placements)==null?void 0:M.home)??{pinnedItems:N.pinnedItems??[],primeSubtitle:N.primeSubtitle??""},movies:((F=N.placements)==null?void 0:F.movies)??fe(),tv_shows:((T=N.placements)==null?void 0:T.tv_shows)??fe()}),p(N.schedules??[]))},[N,u]);const B=()=>r("search",async()=>{const M=f.trim();if(!M)return;const F=await a(()=>q.get(`/admin/api/hero/search?q=${encodeURIComponent(M)}`));F&&b(F.items??[])}),S=g[c],L=S.pinnedItems??[],K=M=>{v(F=>({...F,[c]:{...F[c],...M}})),m(!0)},ne=M=>{if(!L.some(F=>F.id===M.id)){if(L.length>=Dn){d("Remove a pinned title before adding another.","bad");return}K({pinnedItems:[...L,M]})}},X=()=>r("save",async()=>{await a(()=>q.post("/admin/api/hero-policy",{placements:Object.fromEntries(Object.entries(g).map(([M,F])=>[M,{pinnedItemIds:(F.pinnedItems??[]).map(T=>T.id),primeSubtitle:F.primeSubtitle.trim()}])),schedules:k}),"Hero saved."),m(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Featured content",intro:"Manage an independent, backend-resolved hero for Home, Movies and TV Shows."}),e.jsx(U,{message:n}),o?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(oe,{children:je.map(M=>{var le;const F=g[M.id],T=k.filter(me=>me.enabled&&(me.placements??["home"]).includes(M.id)).sort((me,Is)=>Is.priority-me.priority)[0],_=(F.pinnedItems??[]).length?"Manual":T?"Schedule ready":"Automatic",ce=(le=N==null?void 0:N.items)==null?void 0:le.find(me=>me.id===(T==null?void 0:T.itemId)),de=(F.pinnedItems??[]).map(me=>me.name).join(", ")||(ce==null?void 0:ce.name)||(T==null?void 0:T.itemId)||"Resolved for each viewer";return e.jsx(R,{title:M.label,intro:`${_} · ${de}`,tone:M.id===c?"info":void 0,children:e.jsxs(A,{size:"sm",variant:"quiet",onClick:()=>l(M.id),children:["Manage ",M.label]})},M.id)})}),e.jsx("div",{className:"tabs",role:"tablist","aria-label":"Hero placement",children:je.map(M=>e.jsx(A,{variant:c===M.id?"primary":"quiet",onClick:()=>l(M.id),children:M.label},M.id))}),e.jsxs(R,{title:`${($=je.find(M=>M.id===c))==null?void 0:$.label} hero`,intro:`Pinned ${(I=je.find(M=>M.id===c))==null?void 0:I.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(A,{variant:"primary",busy:t==="save",onClick:()=>void X(),children:"Save hero"}),e.jsx(A,{onClick:()=>{K({pinnedItems:[]})},children:"Clear pins"}),u?e.jsx("span",{className:"hint",children:"Unsaved changes."}):null]}),children:[L.length===0?e.jsx(Z,{children:"No titles are pinned. The hero is entirely release-aware and automatic."}):e.jsx("div",{className:"hero-pins",children:L.map((M,F)=>e.jsxs("div",{className:"hero-pin",children:[e.jsx("span",{className:"hero-pin-order",children:F+1}),e.jsxs("span",{children:[e.jsx("b",{children:M.name}),e.jsxs("small",{children:[M.type,M.year?` · ${M.year}`:""]})]}),e.jsx(A,{size:"sm",icon:"clock",onClick:()=>E(ss(M,c)),children:"Schedule"}),e.jsx(A,{variant:"quiet",size:"sm",icon:"close",title:`Remove ${M.name}`,onClick:()=>K({pinnedItems:L.filter(T=>T.id!==M.id)})})]},M.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:S.primeSubtitle,placeholder:"Leave blank for the automatic reason",onChange:M=>{K({primeSubtitle:M.target.value})}})})]}),e.jsx(R,{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(C,{tone:"info",children:(N==null?void 0:N.timeZone)||"server local time"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"primary",busy:t==="save",onClick:()=>void X(),children:"Save schedule"}),e.jsx("span",{className:"hint",children:"Daily and weekly rules repeat until you switch them off."})]}),children:k.length===0?e.jsx(Z,{children:"No scheduled heroes yet. Use Schedule beside a pinned or searched title."}):e.jsx("div",{className:"hero-schedule-list",children:[...k].sort((M,F)=>Number(F.enabled)-Number(M.enabled)||F.priority-M.priority).map(M=>{const F=[...(N==null?void 0:N.items)??[],...L,...j??[]].find(T=>T.id===M.itemId);return e.jsxs("article",{className:"hero-schedule","data-enabled":M.enabled||void 0,children:[e.jsxs("div",{className:"hero-schedule-time",children:[e.jsx("b",{children:M.frequency==="weekly"?"Weekly":M.frequency==="daily"?"Daily":"Once"}),e.jsx("span",{children:M.frequency?M.startTime:M.startAt?new Date(M.startAt).toLocaleDateString():"—"})]}),e.jsxs("div",{className:"hero-schedule-main",children:[e.jsxs("div",{className:"hero-schedule-title",children:[e.jsx("h3",{children:(F==null?void 0:F.name)??M.itemId}),e.jsx(C,{tone:M.enabled?"ok":void 0,children:M.enabled?"enabled":"paused"})]}),e.jsx("p",{children:Fn(M)}),e.jsxs("div",{className:"chips",children:[(M.placements??["home"]).map(T=>{var _;return e.jsx("span",{className:"chip",children:(_=je.find(ce=>ce.id===T))==null?void 0:_.label},T)}),M.priority!==0?e.jsxs("span",{className:"chip",children:["Priority ",M.priority]}):null]})]}),e.jsxs("div",{className:"hero-schedule-actions",children:[e.jsx(A,{size:"sm",onClick:()=>E({...M}),children:"Edit"}),e.jsx(A,{size:"sm",variant:"quiet",onClick:()=>{p(T=>T.map(_=>_.id===M.id?{..._,enabled:!_.enabled}:_)),m(!0)},children:M.enabled?"Pause":"Enable"}),e.jsx(A,{size:"sm",variant:"quiet",onClick:()=>{p(T=>T.filter(_=>_.id!==M.id)),m(!0)},children:"Remove"})]})]},M.id)})})}),e.jsxs(R,{title:"Find a title",intro:`Search the imported Emby catalogue. Add a result to the selected ${(H=je.find(M=>M.id===c))==null?void 0:H.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:f,placeholder:"Search films and television shows",onChange:M=>h(M.target.value),onKeyDown:M=>{M.key==="Enter"&&B()}})}),e.jsx(A,{busy:t==="search",icon:"search",onClick:()=>void B(),children:"Search"})]}),j===null?null:j.length===0?e.jsx(Z,{children:"No playable films or series matched that search."}):e.jsx(oe,{children:j.map(M=>e.jsx(R,{title:M.name,intro:`${M.type||"Title"} · ${M.year||"Year unknown"}`,children:e.jsxs("div",{className:"row",children:[e.jsx(A,{size:"sm",icon:"plus",disabled:L.some(F=>F.id===M.id)||c==="movies"&&M.type!=="Movie"||c==="tv_shows"&&M.type!=="Series",onClick:()=>ne(M),children:L.some(F=>F.id===M.id)?"Pinned":"Add to hero"}),e.jsx(A,{size:"sm",icon:"clock",onClick:()=>E(ss(M,c)),children:"Schedule"})]})},M.id))})]})]}),y?e.jsx(On,{schedule:y,item:[...(N==null?void 0:N.items)??[],...L,...j??[]].find(M=>M.id===y.itemId),timeZone:(N==null?void 0:N.timeZone)||"server local time",isNew:!k.some(M=>M.id===y.id),onCancel:()=>E(null),onSave:M=>{p(F=>F.some(T=>T.id===M.id)?F.map(T=>T.id===M.id?M:T):[...F,M]),E(null),m(!0)}}):null]})}function On({schedule:s,item:n,timeZone:o,isNew:i,onSave:a,onCancel:d}){const[t,r]=x.useState({...s,weekdays:[...s.weekdays??[]]}),c=qn(t),l=h=>{const j=new Date,b=new Date(j.getTime()+2*60*60*1e3);r(k=>{var p;return h==="once"?{...k,frequency:void 0,startAt:es(k.startAt)?k.startAt:j.toISOString(),endAt:es(k.endAt)?k.endAt:b.toISOString()}:{...k,frequency:h,startTime:k.startTime||"18:00",endTime:k.endTime||"22:00",weekdays:h==="weekly"?(p=k.weekdays)!=null&&p.length?k.weekdays:[1,2,3,4,5]:[]}})},g=t.placements??["home"],v=h=>h==="home"||h==="movies"&&(n==null?void 0:n.type)==="Movie"||h==="tv_shows"&&(n==null?void 0:n.type)==="Series",u=!!(t.startAt&&t.endAt&&new Date(t.endAt)>new Date(t.startAt)),m=!!(t.startTime&&t.endTime&&t.startTime!==t.endTime&&(c!=="weekly"||(t.weekdays??[]).length>0)),f=c==="once"?u:m;return e.jsx("div",{className:"scrim",onPointerDown:h=>h.target===h.currentTarget&&d(),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)??t.itemId}),e.jsxs("p",{children:["Choose exactly when this title can lead the selected sections. Times use ",o,"."]})]}),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:()=>l(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(D,{label:"Starts",children:e.jsx("input",{type:"datetime-local",value:t.startAt?Xe(new Date(t.startAt)):"",onChange:h=>r(j=>({...j,startAt:h.target.value?new Date(h.target.value).toISOString():void 0}))})}),e.jsx(D,{label:"Ends",children:e.jsx("input",{type:"datetime-local",value:t.endAt?Xe(new Date(t.endAt)):"",onChange:h=>r(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(D,{label:"Starts each time",children:e.jsx("input",{type:"time",value:t.startTime??"",onChange:h=>r(j=>({...j,startTime:h.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:t.endTime??"",onChange:h=>r(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:()=>r(h=>({...h,weekdays:[1,2,3,4,5]})),children:"Weekdays"}),e.jsx("button",{type:"button",onClick:()=>r(h=>({...h,weekdays:[6,0]})),children:"Weekend"}),e.jsx("button",{type:"button",onClick:()=>r(h=>({...h,weekdays:Ue.map(j=>j.value)})),children:"Every day"})]})]}),e.jsx("div",{className:"schedule-day-grid",children:Ue.map(h=>{const j=(t.weekdays??[]).includes(h.value);return e.jsx("button",{type:"button","aria-pressed":j,title:h.label,onClick:()=>r(b=>({...b,weekdays:j?(b.weekdays??[]).filter(k=>k!==h.value):[...b.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:je.map(h=>e.jsx(z,{label:h.label,checked:g.includes(h.id),disabled:!v(h.id),onChange:j=>r(b=>{const k=b.placements??["home"],p=j?[...k,h.id]:k.filter(y=>y!==h.id);return{...b,placements:p.length?[...new Set(p)]:k}})},h.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:t.priority,onChange:h=>r(j=>({...j,priority:Number(h.target.value)}))})})]}),e.jsx(z,{label:"Schedule enabled",hint:"Pause it without losing its days and times.",checked:t.enabled,onChange:h=>r(j=>({...j,enabled:h}))}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(A,{variant:"quiet",onClick:d,children:"Cancel"}),e.jsx(A,{variant:"primary",disabled:!f,onClick:()=>a(t),children:i?"Add rule":"Save rule"})]})]})})}function Un(){const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(null),l=s==null?void 0:s.features,g=(l==null?void 0:l.features)??[],v=(s==null?void 0:s.clients)??[],u=(l==null?void 0:l.revision)??0,m=(b,k,p,y)=>t(k,async()=>{await a(()=>q.post("/admin/api/features",{action:b,expectedRevision:u,overrides:y??{}}),p),c(null),await i()}),f=v.filter(b=>(b.capabilities??[]).includes("server_features_v1")).length,h=!!(l!=null&&l.safeMode),j=(b,k)=>({...Object.fromEntries(g.filter(p=>p.source==="override").map(p=>[p.key,p.enabled])),[b]:k});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Features",intro:"Roll out, stop and recover optional behaviour with no app release."}),e.jsx(U,{message:n}),o||!l?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(R,{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(A,{variant:h?void 0:"danger",busy:d==="safe",onClick:()=>h?void m("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(Ms,{tiles:[{label:"features active",value:`${g.filter(b=>b.enabled).length} / ${g.length}`},{label:"explicit overrides",value:w(g.filter(b=>b.source==="override").length)},{label:"televisions reporting the control plane",value:`${f} / ${v.length}`},{label:"published revision",value:`r${w(u)}`}]})}),e.jsx(oe,{cols:"2",children:g.length===0?e.jsx(R,{title:"Nothing registered",icon:"sliders",children:e.jsx(Z,{children:"No server features are registered."})}):g.map(b=>e.jsxs(R,{title:b.name,intro:b.description,actions:e.jsx(C,{tone:b.enabled?"ok":void 0,children:b.enabled?"active":"off"}),footer:e.jsxs("span",{className:"hint",children:["↳ ",b.recovery]}),children:[e.jsx(z,{label:b.enabled?"On":"Off",hint:"Changing this applies the feature policy to every compatible television.",checked:b.enabled,disabled:d===b.key,onChange:k=>c({action:"feature",key:b.key,enabled:k,title:`${k?"Turn on":"Turn off"} ${b.name}?`,body:`${k?"Enable":"Disable"} this feature for every compatible television. ${b.recovery}`,label:k?"Turn on":"Turn off"})}),e.jsxs("div",{className:"chips",children:[e.jsx(te,{children:b.key}),e.jsxs(te,{children:["protocol ",w(b.minimumProtocol),"+"]}),e.jsx(te,{tone:b.compatible?"ok":"warn",children:b.compatible?"server compatible":"compatibility blocked"}),e.jsx(te,{tone:"note",children:b.area})]})]},b.key))}),e.jsx(R,{children:e.jsxs("div",{className:"row",children:[e.jsx(A,{disabled:!l.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(A,{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(C,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(C,{tone:"ok",children:["live · revision r",w(u)]})]})})]}),r?e.jsx(he,{title:r.title,body:r.body,confirmLabel:r.label,destructive:r.action!=="rollback",busy:d===r.action,onConfirm:()=>void m(r.action==="feature"?"save":r.action,r.action==="feature"?r.key??"feature":r.action,`${r.label} done.`,r.action==="feature"&&r.key?j(r.key,!!r.enabled):void 0),onCancel:()=>c(null)}):null]})}function Vn(){const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a,show:d}=Y(),{busy:t,run:r}=Q(),[c,l]=x.useState(!0),[g,v]=x.useState("6.5"),[u,m]=x.useState(!1);x.useEffect(()=>{var h,j;u||!s||(l(((h=s.playbackPolicy)==null?void 0:h.prerollEnabled)!==!1),v(String((((j=s.playbackPolicy)==null?void 0:j.prerollDurationMs)??6500)/1e3)))},[s,u]);const f=()=>r("save",async()=>{const h=Number(g);if(!Number.isFinite(h)||h<1||h>30){d("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(h*1e3)}),"Playback policy saved."),m(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Playback",intro:"Presentation policy sent with every playback launch."}),e.jsx(U,{message:n}),o?e.jsx(W,{rows:1}):e.jsxs(R,{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(C,{tone:"ok",children:["on · ",g,"s"]}):e.jsx(C,{children:"off"}),footer:e.jsx(A,{variant:"primary",busy:t==="save",onClick:()=>void f(),children:"Save playback policy"}),children:[e.jsx(z,{label:"Show the preroll before a title starts",checked:c,onChange:h=>{l(h),m(!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:g,onChange:h=>{v(h.target.value),m(!0)}})})})]})]})}function Bn(){const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(null),[l,g]=x.useState(null),[v,u]=x.useState(!1),m=s==null?void 0:s.subtitles,f=m==null?void 0:m.stored;x.useEffect(()=>{r||!m||c({bazarr:m.bazarrEnabled,openSubtitles:m.openSubtitlesEnabled,key:"",clearKey:!1,username:m.openSubtitlesUsername??"",password:"",clearLogin:!1})},[m,r]);const h=p=>c(y=>y&&{...y,...p}),j=()=>t("save",async()=>{r&&(await a(()=>q.post("/admin/api/subtitle-settings",{bazarrEnabled:r.bazarr,openSubtitlesEnabled:r.openSubtitles,openSubtitlesApiKey:r.key.trim(),clearOpenSubtitlesApiKey:r.clearKey,openSubtitlesUsername:r.username.trim(),openSubtitlesPassword:r.password,clearOpenSubtitlesLogin:r.clearLogin}),"Subtitle settings saved."),c(null),await i())}),b=()=>t("test",async()=>{g(null);const p=await a(()=>q.post("/admin/api/subtitle-test"));g((p==null?void 0:p.results)??[])}),k=()=>t("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(O,{title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from."}),e.jsx(U,{message:n}),o||!m||!r?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"offered on televisions",value:m.available?"yes":"no",small:!0,icon:"captions",tone:m.available?"ok":void 0},{label:"providers on",value:w((m.bazarrEnabled&&m.bazarrConfigured?1:0)+(m.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:P(f==null?void 0:f.latest),small:!0,icon:"clock"}]}),e.jsxs(oe,{cols:"2",children:[e.jsx(R,{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:m.bazarrConfigured?r.bazarr?e.jsx(C,{tone:"ok",children:"on"}):e.jsx(C,{children:"off"}):e.jsx(C,{children:"not configured"}),footer:e.jsx("span",{className:"hint",children:m.bazarrConfigured?`Configured at ${m.bazarrUrl}`:"Set MEMBY_BAZARR_URL and MEMBY_BAZARR_API_KEY to use Bazarr."}),children:e.jsx(z,{label:"Offer Bazarr in the player",hint:"Off leaves every subtitle it has already written in place.",checked:r.bazarr,disabled:!m.bazarrConfigured,onChange:p=>h({bazarr:p})})}),e.jsxs(R,{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:m.openSubtitlesEnabled?e.jsx(C,{tone:m.openSubtitlesAccount?"ok":"warn",children:m.openSubtitlesAccount?"on · signed in":"on · anonymous"}):e.jsx(C,{children:m.openSubtitlesKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(z,{label:"Offer OpenSubtitles in the player",hint:"Needs an API key. It cannot be switched on without one.",checked:r.openSubtitles,onChange:p=>h({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:r.key,placeholder:m.openSubtitlesKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:p=>h({key:p.target.value})})}),e.jsx(z,{label:"Remove the saved key",checked:r.clearKey,onChange:p=>h({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:r.username,placeholder:"Not signed in",onChange:p=>h({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:r.password,onChange:p=>h({password:p.target.value})})})]}),e.jsx(z,{label:"Sign out and forget the account",checked:r.clearLogin,onChange:p=>h({clearLogin:p})})]})]}),e.jsxs(R,{children:[e.jsxs("div",{className:"row",children:[e.jsx(A,{variant:"primary",busy:d==="save",onClick:()=>void j(),children:"Save subtitle settings"}),e.jsx(A,{busy:d==="test",icon:"pulse",onClick:()=>void b(),children:"Test the providers"}),e.jsx("span",{className:"hint",children:m.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."})]}),l===null?null:l.length===0?e.jsx(Z,{children:"No provider is switched on, so there was nothing to ask."}):e.jsx("div",{className:"list",children:l.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(C,{tone:p.ok?"ok":"bad",children:p.ok?"reachable":"not reachable"})})]},p.provider))})]}),e.jsx(R,{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(C,{tone:"data",children:[w(f.count)," files · ",ye(f.bytes)]}):e.jsx(C,{children:"nothing held"}),footer:e.jsx(A,{variant:"danger",disabled:!(f!=null&&f.count),onClick:()=>u(!0),children:"Delete every stored subtitle"}),children:e.jsx(e.Fragment,{})})]}),v?e.jsx(he,{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:d==="clear",onConfirm:()=>void k(),onCancel:()=>u(!1)}):null]})}function _n(){const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(null),[l,g]=x.useState(!1),v=s==null?void 0:s.updatePolicy;x.useEffect(()=>{r||!v||c({version:v.latestVersion??"",url:v.downloadUrl??"",notes:v.notes??"",retireBelow:v.retireBelowVersion??"",required:!!v.minimumVersion&&v.minimumVersion===v.latestVersion,destructive:!!v.retireBelowVersion&&v.retireBelowVersion===v.latestVersion})},[v,r]);const u=j=>t(j?"save":"off",async()=>{r&&(await a(()=>q.post("/admin/api/update-policy",{enabled:j,latestVersion:r.version.trim(),downloadUrl:r.url.trim(),notes:r.notes.trim(),required:r.required,destructive:r.destructive,retireBelowVersion:r.retireBelow.trim()}),j?"Update policy saved.":"Update prompts turned off."),g(!1),c(null),await i())}),m=!!(v!=null&&v.minimumVersion)&&(v==null?void 0:v.minimumVersion)===(v==null?void 0:v.latestVersion),f=!!(v!=null&&v.retireBelowVersion)&&(v==null?void 0:v.retireBelowVersion)===(v==null?void 0:v.latestVersion),h=j=>c(b=>b&&{...b,...j});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"App updates",intro:"Publish an optional or a required client update."}),e.jsx(U,{message:n}),o||!r?e.jsx(W,{rows:1}):e.jsxs(R,{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:v!=null&&v.enabled?e.jsxs(C,{tone:m?"warn":"ok",children:[f?"sign-out · ":m?"required · ":"optional · ",v.latestVersion]}):e.jsx(C,{children:"off"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"primary",busy:d==="save",onClick:()=>r.required?g(!0):void u(!0),children:"Save policy"}),e.jsx(A,{busy:d==="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:r.version,placeholder:"0.2.63",onChange:j=>h({version:j.target.value})})}),e.jsx(D,{label:"APK URL",children:e.jsx("input",{type:"text",value:r.url,placeholder:"https://nas/memby/memby-0.2.63.apk",onChange:j=>h({url:j.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:r.notes,placeholder:"One line the viewer reads",onChange:j=>h({notes:j.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:r.retireBelow,placeholder:"0.2.44",onChange:j=>h({retireBelow:j.target.value})})}),e.jsx(z,{label:"Require this update",hint:"Blocks the home screen on every television below this version.",checked:r.required,onChange:j=>h({required:j})}),e.jsx(z,{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:r.destructive,onChange:j=>h(j?{destructive:!0,required:!0,retireBelow:r.version.trim()}:{destructive:!1,retireBelow:r.retireBelow.trim()===r.version.trim()?"":r.retireBelow})})]}),l&&r?e.jsx(he,{title:r.destructive?"Sign every older television out?":"Require this update?",body:r.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:r.destructive,busy:d==="save",onConfirm:()=>void u(!0),onCancel:()=>g(!1)}):null]})}const Wn=2e4,zn=3e3,Hn=[{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 Kn(s){const n=[...Hn];for(const o of[s.defaultIntervalSeconds,s.intervalSeconds])o>0&&!n.some(i=>i.value===o)&&n.push({value:o,label:Pe(o).replace(/^every /,"Every ")});return n.sort((o,i)=>o.value-i.value)}function ns(s){return s==="failed"?"bad":s==="running"?"info":s==="skipped"?"warn":"ok"}function Gn(){const{wrap:s}=Y(),{busy:n,run:o}=Q(),[i,a]=x.useState(!1),{data:d,error:t,loading:r,reload:c}=J("/admin/api/tasks?limit=60",{pollMs:i?zn:Wn}),l=(d==null?void 0:d.tasks)??[],g=l.some(y=>y.running);g!==i&&a(g);const v=y=>o(y.id,async()=>{await s(()=>q.post(`/admin/api/tasks/${encodeURIComponent(y.id)}/run`),`${y.name} started.`),await c()}),u=(y,E)=>o(`${y.id}:enabled`,async()=>{await s(()=>q.put(`/admin/api/tasks/${encodeURIComponent(y.id)}`,{enabled:E}),E?`${y.name} switched on.`:`${y.name} switched off.`),await c()}),m=(y,E)=>o(`${y.id}:interval`,async()=>{await s(()=>q.put(`/admin/api/tasks/${encodeURIComponent(y.id)}`,{intervalSeconds:E}),`${y.name} now runs ${Pe(E)}.`),await c()}),f=y=>o(`${y.id}:interval`,async()=>{await s(()=>q.put(`/admin/api/tasks/${encodeURIComponent(y.id)}`,{intervalSeconds:0}),`${y.name} back to its default cadence.`),await c()}),h=l.filter(y=>{var E;return((E=y.lastRun)==null?void 0:E.status)==="failed"}).length,j=l.filter(y=>y.defaultIntervalSeconds>0&&y.intervalSeconds!==y.defaultIntervalSeconds).length,b=l.filter(y=>!y.enabled).length,k=(d==null?void 0:d.groups)??[],p=l.filter(y=>!y.group);return e.jsxs(e.Fragment,{children:[e.jsx(O,{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(U,{message:t}),r?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"Tasks",value:w(l.length),icon:"clock",tone:"info"},{label:"Running now",value:w(l.filter(y=>y.running).length),icon:"pulse",tone:g?"ok":void 0},{label:"Last run failed",value:w(h),icon:"alert",tone:h>0?"bad":void 0},{label:"Switched off",value:w(b),icon:"power",tone:b>0?"warn":void 0},{label:"Retimed",value:w(j),icon:"clock",tone:j>0?"note":void 0}]}),h>0?e.jsx(ue,{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,[...k,...p.length>0?[""]:[]].map(y=>{const E=l.filter(N=>N.group===y);return E.length===0?null:e.jsx(R,{title:y||"Other",icon:y==="System"?"chip":y==="Analytics"?"chart":"wrench",tone:y==="System"?"info":y==="Analytics"?"data":"note",children:e.jsx("div",{className:"list",children:E.map(N=>{var B;return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[N.name," ",N.running?e.jsx(C,{tone:"info",children:"running"}):null,N.enabled?null:e.jsx(C,{tone:"warn",children:"off"}),N.defaultIntervalSeconds>0&&N.intervalSeconds!==N.defaultIntervalSeconds?e.jsx(C,{tone:"note",children:"retimed"}):null]}),e.jsx("p",{children:N.description}),e.jsxs("p",{className:"quiet",children:[Pe(N.intervalSeconds),N.enabled&&N.nextRun?` · next ${pe(N.nextRun).replace(" ago","")}`:"",N.lastRun?e.jsxs(e.Fragment,{children:[" · last ",e.jsx("span",{title:P(N.lastRun.startedAt),children:pe(N.lastRun.startedAt)}),` in ${be(N.lastRun.durationMs)}`,N.lastRun.detail?` — ${N.lastRun.detail}`:""]}):" · never run"]}),(B=N.lastRun)!=null&&B.error?e.jsx("p",{className:"mono",style:void 0,children:e.jsx(C,{tone:"bad",children:N.lastRun.error})}):null]}),e.jsxs("div",{className:"list-actions",children:[N.lastRun?e.jsx(C,{tone:ns(N.lastRun.status),children:N.lastRun.status}):e.jsx(C,{children:"never run"}),e.jsx("select",{"aria-label":`How often ${N.name} runs`,value:N.intervalSeconds,disabled:n===`${N.id}:interval`||N.running,onChange:S=>void m(N,Number(S.target.value)),children:Kn(N).map(S=>e.jsxs("option",{value:S.value,children:[S.label,S.value===N.defaultIntervalSeconds?" (default)":""]},S.value))}),N.defaultIntervalSeconds>0&&N.intervalSeconds!==N.defaultIntervalSeconds?e.jsx(A,{size:"sm",icon:"refresh",busy:n===`${N.id}:interval`,onClick:()=>void f(N),children:"Default"}):null,e.jsx(z,{label:"",checked:N.enabled,disabled:n===`${N.id}:enabled`,onChange:S=>void u(N,S)}),e.jsx(A,{size:"sm",icon:"play",busy:n===N.id,disabled:N.running,onClick:()=>void v(N),children:"Run now"})]})]},N.id)})})},y||"other")}),e.jsx(R,{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(G,{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:((d==null?void 0:d.runs.length)??0)===0?e.jsx(se,{columns:6,children:"No task has run yet."}):d==null?void 0:d.runs.map(y=>{var E;return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(y.startedAt),children:pe(y.startedAt)}),e.jsx("td",{children:((E=l.find(N=>N.id===y.taskId))==null?void 0:E.name)??y.taskId}),e.jsx("td",{className:"muted",children:y.trigger}),e.jsx("td",{children:e.jsx(C,{tone:ns(y.status),children:y.status})}),e.jsx("td",{className:"num muted",children:be(y.durationMs)}),e.jsx("td",{className:"muted",children:y.error||y.detail||"—"})]},y.id)})})]})})})]})]})}const Zn={id:"",name:"Discord",url:"",enabled:!0,events:[]};function Jn(){const{wrap:s,show:n}=Y(),{busy:o,run:i}=Q(),{data:a,error:d,loading:t,reload:r}=J("/admin/api/integrations",{pollMs:6e4}),[c,l]=x.useState(null),[g,v]=x.useState(null),u=(a==null?void 0:a.catalogue)??[],m=(a==null?void 0:a.integrations)??[],f=k=>l({id:k.id,name:k.name,url:"",enabled:k.enabled,events:k.events??[]}),h=()=>i("save",async()=>{if(!c)return;await s(()=>q.post("/admin/api/integrations",c),c.id?"Integration saved.":"Integration added.")&&(l(null),await r())}),j=k=>i("remove",async()=>{await s(()=>q.del(`/admin/api/integrations/${encodeURIComponent(k.id)}`),`${k.name} removed.`),v(null),await r()}),b=k=>i(`test:${k.id}`,async()=>{const p=await s(()=>q.post(`/admin/api/integrations/${encodeURIComponent(k.id)}/test`));p&&n(p.message,p.ok?"ok":"bad"),await r()});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Integrations",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(A,{variant:"primary",icon:"plus",onClick:()=>l(Zn),children:"Add a webhook"})}),e.jsx(U,{message:d}),e.jsx(Qn,{}),e.jsx(Yn,{}),e.jsx(Xn,{}),((a==null?void 0:a.dropped)??0)>0?e.jsxs(ue,{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,t?e.jsx(W,{}):m.length===0&&!c?e.jsx(R,{title:"Nothing configured",icon:"plug",tone:"note",children:e.jsx(Z,{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."})}):m.map(k=>e.jsx(et,{integration:k,catalogue:u,busy:o,onEdit:()=>f(k),onTest:()=>void b(k),onRemove:()=>v(k)},k.id)),c?e.jsx(st,{draft:c,catalogue:u,busy:o==="save",onChange:l,onSave:()=>void h(),onCancel:()=>l(null)}):null,g?e.jsx(he,{title:`Remove ${g.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:o==="remove",onConfirm:()=>void j(g),onCancel:()=>v(null)}):null]})}function Qn(){const{wrap:s}=Y(),{busy:n,run:o}=Q(),{data:i,error:a,loading:d,reload:t}=J("/admin/api/arr-integrations"),r=c=>o("arr-integrations",async()=>{i&&(await s(()=>q.post("/admin/api/arr-integrations",{sonarrEnabled:c.sonarrEnabled??i.sonarrEnabled,radarrEnabled:c.radarrEnabled??i.radarrEnabled}),"Integration settings saved."),await t())});return e.jsxs(R,{title:"Sonarr and Radarr",intro:"Turn either service off without removing its address, API key or request policy. Disabled services are not offered for Memby requests.",icon:"plug",tone:"info",children:[e.jsx(U,{message:a??""}),d?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(z,{label:"Sonarr enabled",hint:i!=null&&i.sonarrConfigured?"Off stops Memby sending or looking up TV requests through Sonarr.":"Sonarr is not configured.",checked:!!(i!=null&&i.sonarrEnabled),disabled:!(i!=null&&i.sonarrConfigured)||n==="arr-integrations",onChange:c=>void r({sonarrEnabled:c})}),e.jsx(z,{label:"Radarr enabled",hint:i!=null&&i.radarrConfigured?"Off stops Memby sending or looking up film requests through Radarr.":"Radarr is not configured.",checked:!!(i!=null&&i.radarrEnabled),disabled:!(i!=null&&i.radarrConfigured)||n==="arr-integrations",onChange:c=>void r({radarrEnabled:c})})]})]})}function Yn(){const{wrap:s}=Y(),{busy:n,run:o}=Q(),{data:i,error:a,loading:d,reload:t}=J("/admin/api/sonarr-request-policy"),[r,c]=x.useState(0),[l,g]=x.useState(!1);x.useEffect(()=>{i&&(c(i.qualityProfileId),g(i.searchImmediately))},[i]);const v=()=>o("sonarr-request-policy",async()=>{await s(()=>q.post("/admin/api/sonarr-request-policy",{qualityProfileId:r,searchImmediately:l}),"Sonarr TV request policy saved."),await t()}),u=i==null?void 0:i.profiles.find(m=>m.id===r);return e.jsxs(R,{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(C,{tone:"ok",children:"configured"}):e.jsx(C,{tone:"warn",children:"needs attention"}),footer:e.jsx(A,{variant:"primary",busy:n==="sonarr-request-policy",disabled:d||r<=0,onClick:()=>void v(),children:"Save Sonarr policy"}),children:[e.jsx(U,{message:a??(i==null?void 0:i.error)??""}),d?e.jsx(W,{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:r,onChange:m=>c(Number(m.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(m=>e.jsxs("option",{value:m.id,children:[m.name,m.recommended?" — recommended (720p)":""]},m.id))]})})}),e.jsx(z,{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:l,onChange:g}),u?e.jsxs(ue,{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 ",l?"start an immediate search.":"not start an immediate search."]}):null]})]})}function Xn(){const{wrap:s}=Y(),{busy:n,run:o}=Q(),{data:i,error:a,loading:d,reload:t}=J("/admin/api/radarr-request-policy"),[r,c]=x.useState(0),[l,g]=x.useState(!1);x.useEffect(()=>{i&&(c(i.qualityProfileId),g(i.searchImmediately))},[i]);const v=()=>o("radarr-request-policy",async()=>{await s(()=>q.post("/admin/api/radarr-request-policy",{qualityProfileId:r,searchImmediately:l}),"Radarr movie request policy saved."),await t()}),u=i==null?void 0:i.profiles.find(m=>m.id===r);return e.jsxs(R,{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(C,{tone:"ok",children:"configured"}):e.jsx(C,{tone:"warn",children:"needs attention"}),footer:e.jsx(A,{variant:"primary",busy:n==="radarr-request-policy",disabled:d||r<=0,onClick:()=>void v(),children:"Save Radarr policy"}),children:[e.jsx(U,{message:a??(i==null?void 0:i.error)??""}),d?e.jsx(W,{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:r,onChange:m=>c(Number(m.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(m=>e.jsxs("option",{value:m.id,children:[m.name,m.recommended?" — recommended (720p)":""]},m.id))]})})}),e.jsx(z,{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:l,onChange:g}),u?e.jsxs(ue,{tone:"info",children:["Requested films will use ",e.jsx("b",{children:u.name})," (profile ID ",u.id,"), remain monitored, and ",l?"start an immediate search.":"not start an immediate search."]}):null]})]})}function et({integration:s,catalogue:n,busy:o,onEdit:i,onTest:a,onRemove:d}){const t=s.health,r=!t.lastFailure||t.lastSuccess&&t.lastSuccess>t.lastFailure,c=s.events??[];return e.jsxs(R,{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(C,{tone:"ok",children:"on"}):e.jsx(C,{tone:"warn",children:"off"}),t.deliveries>0?e.jsx(C,{tone:r?"ok":"bad",children:r?"delivering":"failing"}):e.jsx(C,{children:"never used"}),e.jsx(A,{size:"sm",icon:"pulse",busy:o===`test:${s.id}`,onClick:a,children:"Test"}),e.jsx(A,{size:"sm",onClick:i,children:"Edit"}),e.jsx(A,{size:"sm",variant:"danger",icon:"trash",onClick:d,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(l=>{var g;return((g=n.find(v=>v.type===l))==null?void 0:g.label)??l}).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:t.lastSuccess?P(t.lastSuccess):"never"})]}),e.jsx("div",{className:"list-actions",children:t.deliveries>0?e.jsxs("span",{className:"quiet",children:[w(t.deliveries)," attempts, ",w(t.failures)," failed"]}):null})]}),t.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:[P(t.lastFailure),t.lastError?` — ${t.lastError}`:""]})]})}):null]}),s.deliveries.length>0?e.jsx(G,{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(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(l.attemptedAt),children:pe(l.attemptedAt)}),e.jsx("td",{className:"muted",children:l.eventType}),e.jsx("td",{children:l.success?e.jsx(C,{tone:"ok",children:l.statusCode||"ok"}):e.jsx(C,{tone:"bad",children:l.error||l.statusCode||"failed"})}),e.jsx("td",{className:"num muted",children:be(l.durationMs)})]},l.id))})]})}):e.jsx(G,{children:e.jsx("table",{children:e.jsx("tbody",{children:e.jsx(se,{columns:4,children:"Nothing has been delivered through this webhook yet."})})})})]})}function st({draft:s,catalogue:n,busy:o,onChange:i,onSave:a,onCancel:d}){const t=[...new Set(n.map(c=>c.group))],r=(c,l)=>i({...s,events:l?[...s.events,c]:s.events.filter(g=>g!==c)});return e.jsxs(R,{title:s.id?`Edit ${s.name}`:"New Discord webhook",icon:"plug",tone:"info",footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"primary",busy:o,onClick:a,children:s.id?"Save":"Add"}),e.jsx(A,{variant:"quiet",onClick:d,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(z,{label:"Enabled",hint:"Off keeps the configuration and stops the posts.",checked:s.enabled,onChange:c=>i({...s,enabled:c})}),t.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(l=>l.group===c).map(l=>e.jsx(z,{label:l.label,hint:l.description,checked:s.events.includes(l.type),onChange:g=>r(l.type,g)},l.type))]},c))]})}function nt(){var K,ne,X,$;const{status:s,error:n,loading:o,reload:i}=ae(),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(""),[l,g]=x.useState(!1),[v,u]=x.useState(!1),[m,f]=x.useState(!1),[h,j]=x.useState("23:00"),[b,k]=x.useState("07:00"),[p,y]=x.useState(""),[E,N]=x.useState(!1),B=!!((K=s==null?void 0:s.maintenance)!=null&&K.enabled);x.useEffect(()=>{var I;!v&&s&&c(((I=s.maintenance)==null?void 0:I.message)??"")},[s,v]),x.useEffect(()=>{E||!(s!=null&&s.quietTime)||(f(s.quietTime.enabled),j(s.quietTime.startTime),k(s.quietTime.endTime),y(s.quietTime.message))},[s,E]);const S=I=>t(I?"on":"off",async()=>{await a(()=>q.post("/admin/api/maintenance",{enabled:I,message:r}),I?"Memby is offline for every television.":"Memby is back online."),g(!1),u(!1),await i()}),L=()=>t("quiet",async()=>{await a(()=>q.post("/admin/api/quiet-time",{enabled:m,startTime:h,endTime:b,message:p}),m?"Quiet time saved.":"Quiet time turned off."),N(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time."}),e.jsx(U,{message:n}),o?e.jsx(W,{rows:1}):e.jsx(R,{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:B?"bad":"warn",actions:B?e.jsx(C,{tone:"bad",children:"offline"}):e.jsx(C,{tone:"ok",children:"online"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"danger",disabled:B,onClick:()=>g(!0),children:"Go offline"}),e.jsx(A,{disabled:!B,busy:d==="off",onClick:()=>void S(!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:r,placeholder:"Back shortly — upgrading the server",onChange:I=>{c(I.target.value),u(!0)}})})}),o?null:e.jsxs(R,{title:"Quiet time",intro:`Pause new television requests and server background work every day in ${((ne=s==null?void 0:s.quietTime)==null?void 0:ne.timeZone)??"the household 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:(X=s==null?void 0:s.quietTime)!=null&&X.active?"warn":"info",actions:($=s==null?void 0:s.quietTime)!=null&&$.active?e.jsx(C,{tone:"warn",children:"active now"}):m?e.jsx(C,{tone:"ok",children:"scheduled"}):e.jsx(C,{children:"off"}),footer:e.jsx(A,{variant:"primary",busy:d==="quiet",onClick:()=>void L(),children:"Save quiet time"}),children:[e.jsx(z,{label:"Pause server activity during quiet time",checked:m,onChange:I=>{f(I),N(!0)}}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts",hint:"Uses the household's 24-hour clock.",children:e.jsx("input",{type:"time",value:h,onChange:I=>{j(I.target.value),N(!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:b,onChange:I=>{k(I.target.value),N(!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:I=>{y(I.target.value),N(!0)}})})]}),l?e.jsx(he,{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:d==="on",onConfirm:()=>void S(!0),onCancel:()=>g(!1)}):null]})}const tt=-1;function Me(s){return s===0?"":s<0?"off":String(s)}function Ee(s,n){const o=s.trim().toLowerCase();if(o==="")return 0;if(n&&(o==="off"||o==="none"||o==="0"))return tt;const i=Number.parseInt(o,10);return Number.isFinite(i)?i:0}function Ae(s,n){return s<=0?"off":`${s} ${n}${s===1?"":"s"}`}function qe(s){return{timezone:s.timezone??"",logLevel:s.logLevel??"",sessionIdleDays:Me(s.sessionIdleDays),sonarrAlertMinutes:Me(s.sonarrAlertMinutes),radarrAlertMinutes:Me(s.radarrAlertMinutes),embyHealthSeconds:Me(s.embyHealthSeconds)}}function it(){const{data:s,error:n,loading:o,reload:i}=J("/admin/api/gateway-settings"),{wrap:a}=Y(),{busy:d,run:t}=Q(),[r,c]=x.useState(null);x.useEffect(()=>{!r&&s&&c(qe(s.settings))},[s,r]);const l=(h,j)=>c(b=>b&&{...b,[h]:j}),g=()=>t("save",async()=>{if(!r)return;const h={timezone:r.timezone.trim(),logLevel:r.logLevel.trim(),sessionIdleDays:Ee(r.sessionIdleDays,!1),sonarrAlertMinutes:Ee(r.sonarrAlertMinutes,!0),radarrAlertMinutes:Ee(r.radarrAlertMinutes,!0),embyHealthSeconds:Ee(r.embyHealthSeconds,!0)},j=await a(()=>q.post("/admin/api/gateway-settings",h),"Gateway settings saved.");j&&c(qe(j.settings)),await i()}),v=()=>t("clear",async()=>{const h=await a(()=>q.post("/admin/api/gateway-settings",{timezone:"",logLevel:"",sessionIdleDays:0,sonarrAlertMinutes:0,radarrAlertMinutes:0,embyHealthSeconds:0}),"Every setting is back to what this container was deployed with.");h&&c(qe(h.settings)),await i()}),u=s==null?void 0:s.deployed,m=s==null?void 0:s.effective,f=(s==null?void 0:s.logLevels)??[];return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Gateway settings",intro:"Server-level settings for this gateway, changeable without a redeployment."}),e.jsx(U,{message:n}),o||!r||!u||!m?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(R,{title:"This gateway",intro:"What the process is running and what it currently believes.",icon:"chip",tone:"info",actions:e.jsx(C,{tone:"info",children:(s==null?void 0:s.version)??"unknown"}),children:e.jsx(Oe,{rows:[{label:"Household timezone",value:m.timezone||"not set"},{label:"Log level",value:m.logLevel},{label:"Sign-in expiry",value:Ae(m.sessionIdleDays,"day")},{label:"Emby health probe",value:Ae(m.embyHealthSeconds,"second")},{label:"Episode alert window",value:Ae(m.sonarrAlertMinutes,"minute")},{label:"Film alert window",value:Ae(m.radarrAlertMinutes,"minute")}]})}),e.jsxs(R,{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(A,{variant:"primary",busy:d==="save",onClick:()=>void g(),children:"Save settings"}),e.jsx(A,{busy:d==="clear",onClick:()=>void v(),children:"Use deployed values"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Household 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:r.timezone,placeholder:u.timezone,onChange:h=>l("timezone",h.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:r.logLevel,onChange:h=>l("logLevel",h.target.value),children:[e.jsxs("option",{value:"",children:["Deployed (",u.logLevel,")"]}),f.map(h=>e.jsx("option",{value:h,children:h},h))]})})]}),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:r.sessionIdleDays,placeholder:String(u.sessionIdleDays),onChange:h=>l("sessionIdleDays",h.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:r.embyHealthSeconds,placeholder:String(u.embyHealthSeconds),onChange:h=>l("embyHealthSeconds",h.target.value)})})]}),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:r.sonarrAlertMinutes,placeholder:String(u.sonarrAlertMinutes),onChange:h=>l("sonarrAlertMinutes",h.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:r.radarrAlertMinutes,placeholder:String(u.radarrAlertMinutes),onChange:h=>l("radarrAlertMinutes",h.target.value)})})]}),e.jsxs(ue,{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(ue,{children:["Last changed by ",s.settings.updatedBy,s.settings.updatedAt?` on ${new Date(s.settings.updatedAt).toLocaleString("en-NZ")}`:"","."]}):null]})]})]})}function at(){const{status:s,error:n,loading:o}=ae(),i=(s==null?void 0:s.runs)??[];return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Imports",intro:"Catalogue synchronisation history."}),e.jsx(U,{message:n}),o?e.jsx(W,{rows:1}):e.jsx(R,{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.",icon:"sync",tone:"data",children:e.jsx(G,{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(se,{columns:8,children:"Nothing has been imported yet."}):i.map(a=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(a.startedAt)}),e.jsx("td",{children:a.kind}),e.jsx("td",{className:"muted",children:a.trigger}),e.jsx("td",{children:e.jsx(C,{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 ts={TRACE:5,DEBUG:10,INFO:20,WARN:30,ERROR:40},is=2e4,rt=5e3,we=48,lt=31,as=8,ot=["component","user","device","client","protocol","method","path","status","duration","version","gateway_version"],rs=new Map(ot.map((s,n)=>[s,n])),ct=new Intl.DateTimeFormat(void 0,{dateStyle:"short",timeStyle:"medium"}),ls=new WeakMap;function ke(s){const n=ls.get(s);if(n)return n;const o=Object.entries(s.attributes??{}).sort((a,d)=>{const t=rs.get(a[0])??(a[0]==="error"?1e3:100),r=rs.get(d[0])??(d[0]==="error"?1e3:100);return t-r||a[0].localeCompare(d[0])}),i={fields:o,summary:o.map(([a,d])=>`${a}=${String(d)}`).join(" "),haystack:[s.message,...o.flat()].join(" ").toLowerCase(),occurred:ct.format(new Date(s.occurredAt))};return ls.set(s,i),i}const dt=s=>s.replace(/_/g," "),ht=x.memo(function({event:n,index:o,onInspect:i}){const a=ke(n);return e.jsxs("div",{className:"logline","data-level":n.level,"data-virtual":"true",style:{transform:`translateY(${o*we}px)`},children:[e.jsxs("time",{title:n.occurredAt,children:[a.occurred,e.jsxs("small",{children:["#",n.sequence]})]}),e.jsx("span",{className:"lvl",children:n.level}),e.jsx("span",{className:"msg",title:n.message,children:n.message}),e.jsx("button",{type:"button",className:"logattrs-button",title:a.summary||"No structured details",onClick:()=>i(n.sequence),children:a.summary||"View record"})]})});function ut(){var F;const[s,n]=x.useState([]),[o,i]=x.useState(0),[a,d]=x.useState(!1),[t,r]=x.useState("INFO"),[c,l]=x.useState(""),[g,v]=x.useState(""),[u,m]=x.useState({top:0,height:600}),[f,h]=x.useState(null),j=x.useDeferredValue(c.trim().toLowerCase()),b=x.useRef(0),k=x.useRef(!1),p=x.useRef(0),y=x.useRef(null),E=x.useRef(!0),N=x.useRef(void 0),B=x.useCallback(async()=>{if(a||k.current||document.hidden)return;k.current=!0;const T=p.current,_=[];let ce=0;try{let de=0,le;do le=await q.get(`/admin/api/events?after=${b.current}&limit=1000`),b.current=le.next||b.current,ce+=le.dropped||0,_.push(...le.events??[]),de+=1;while(le.hasMore&&de<20);v("")}catch(de){v(de instanceof Error?de.message:String(de))}finally{_.length>0&&T===p.current&&n(de=>{const le=de.concat(_);return le.length>is?le.slice(le.length-is):le}),ce>0&&T===p.current&&i(de=>de+ce),k.current=!1}},[a]);x.useEffect(()=>{if(a)return;let T;const _=()=>{window.clearInterval(T),T=document.hidden?void 0:window.setInterval(()=>void B(),rt)},ce=()=>{_(),document.hidden||B()};return B(),_(),document.addEventListener("visibilitychange",ce),()=>{window.clearInterval(T),document.removeEventListener("visibilitychange",ce)}},[B,a]);const S=x.useMemo(()=>{const T=ts[t]??20;return s.filter(_=>(ts[_.level]??0)>=T&&(!j||ke(_).haystack.includes(j)))},[s,t,j]),L=((F=S.at(-1))==null?void 0:F.sequence)??0,K=Math.max(0,u.top-lt),ne=Math.ceil(u.height/we)+as*2,X=Math.min(Math.max(0,Math.floor(K/we)-as),Math.max(0,S.length-ne)),$=S.slice(X,X+ne),I=x.useMemo(()=>s.find(T=>T.sequence===f),[s,f]);x.useLayoutEffect(()=>{const T=y.current;!T||!E.current||(T.scrollTop=T.scrollHeight,m({top:T.scrollTop,height:T.clientHeight}))},[L,j,t]),x.useEffect(()=>()=>window.cancelAnimationFrame(N.current??0),[]);const H=()=>{const T=y.current;T&&(E.current=T.scrollHeight-T.scrollTop-T.clientHeight{m({top:T.scrollTop,height:T.clientHeight})}))},M=()=>{const T=new Blob([JSON.stringify(s,null,2)],{type:"application/json"}),_=document.createElement("a");_.href=URL.createObjectURL(T),_.download=`memby-events-${new Date().toISOString().replace(/[:.]/g,"-")}.json`,_.click(),window.setTimeout(()=>URL.revokeObjectURL(_.href),1e3)};return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Server logs",intro:"Structured gateway events as they happen."}),e.jsx(U,{message:g}),e.jsxs(R,{children:[e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Level",children:e.jsxs("select",{value:t,onChange:T=>r(T.target.value),children:[e.jsx("option",{value:"DEBUG",children:"Debug and above"}),e.jsx("option",{value:"INFO",children:"Info and above"}),e.jsx("option",{value:"WARN",children:"Warnings and errors"}),e.jsx("option",{value:"ERROR",children:"Errors only"})]})}),e.jsx(D,{label:"Filter",grow:!0,children:e.jsx("input",{type:"search",value:c,placeholder:"Person, television, title, component, path…",onChange:T=>l(T.target.value)})}),e.jsxs("div",{className:"filter-actions",children:[e.jsx(A,{onClick:()=>d(T=>!T),icon:a?"play":"clock",children:a?"Resume":"Pause"}),e.jsx(A,{onClick:()=>{p.current+=1,n([]),i(0),h(null)},children:"Clear view"}),e.jsx(A,{onClick:M,icon:"download",children:"Export JSON"})]})]}),e.jsxs("div",{className:"logview",ref:y,onScroll:H,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:"Event"}),e.jsx("span",{children:"Details"})]}),S.length===0?e.jsx("p",{className:"empty",children:s.length===0?"Waiting for server events…":"No events match this filter."}):e.jsx("div",{className:"logbody",style:{height:`${S.length*we}px`},children:$.map((T,_)=>e.jsx(ht,{event:T,index:X+_,onInspect:h},T.sequence))})]}),e.jsxs("p",{className:"hint",children:[w(s.length)," retained · ",w(S.length)," matching",S.length?` · ${w($.length)} rows mounted`:"",o?` · ${w(o)} overwritten before delivery`:"",a?" · paused":""]}),I?e.jsxs("section",{className:"log-inspector","aria-label":`Log record ${I.sequence}`,children:[e.jsxs("div",{className:"log-inspector-head",children:[e.jsxs("div",{children:[e.jsx("b",{children:I.message}),e.jsxs("span",{children:[ke(I).occurred," · ",I.level," · record #",I.sequence]})]}),e.jsx(A,{size:"sm",variant:"quiet",onClick:()=>h(null),children:"Close"})]}),ke(I).fields.length?e.jsx("dl",{children:ke(I).fields.map(([T,_])=>e.jsxs(x.Fragment,{children:[e.jsx("dt",{children:dt(T)}),e.jsx("dd",{children:String(_)})]},T))}):e.jsx(ue,{children:"No structured details were attached to this record."})]}):null]})]})}const os=["home","movies","shows","favorites","search","recent_searches","genre_browse","for_you","for_you_time","recommendation","continue","latest","my_shows","details","playback","notifications","profiles","settings"];function xe(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function mt(){const[s,n]=x.useState(30),[o,i]=x.useState(""),a=Ve(),d=x.useMemo(()=>`/admin/api/journeys${Se({days:s,userId:o})}`,[s,o]),{data:t,error:r,loading:c}=J(d),l=t==null?void 0:t.stats,g=(t==null?void 0:t.users)??[],v=(t==null?void 0:t.actions)??[],u=(t==null?void 0:t.paths)??[],m=u[0],f=x.useMemo(()=>{const h=new Map(((t==null?void 0:t.features)??[]).map(b=>[b.feature,b])),j=new Map(os.map((b,k)=>[b,k]));return[...new Set([...os,...h.keys()])].map(b=>({name:b,stat:h.get(b)})).sort((b,k)=>{var y,E;const p=(((y=k.stat)==null?void 0:y.uses)??0)-(((E=b.stat)==null?void 0:E.uses)??0);return p||(j.get(b.name)??Number.MAX_SAFE_INTEGER)-(j.get(k.name)??Number.MAX_SAFE_INTEGER)})},[t==null?void 0:t.features]);return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"User journeys",intro:"How viewers move through Memby, use features and complete flows."}),e.jsx(U,{message:r}),e.jsx(R,{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: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(D,{label:"User",children:e.jsxs("select",{value:o,onChange:h=>{const j=h.target.value;i(j),j&&a(`/admin/journeys/${encodeURIComponent(j)}`)},children:[e.jsx("option",{value:"",children:"All users"}),g.map(h=>e.jsx("option",{value:h.userId,children:h.username||h.userId},h.userId))]})})]}),children:c?e.jsx(W,{rows:1}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:"journeys",value:w(l==null?void 0:l.journeys),icon:"list",tone:"data"},{label:"viewers",value:w(l==null?void 0:l.viewers),icon:"people",tone:"info"},{label:"completion",value:Re(l==null?void 0:l.completionRate),icon:"check",tone:"ok"},{label:"abandoned",value:w(l==null?void 0:l.abandoned),icon:"alert",tone:"note"},{label:"active now",value:w(l==null?void 0:l.active),icon:"pulse",tone:"info"},{label:"average steps",value:((l==null?void 0:l.averageSteps)??0).toFixed(1),icon:"chart"},{label:"average visit",value:be(l==null?void 0:l.averageTimeMs),small:!0,icon:"clock"},{label:"history kept",value:`${(t==null?void 0:t.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:Re(l==null?void 0:l.completionRate)})]}),e.jsx(nn,{value:(l==null?void 0:l.completed)??0,total:(l==null?void 0:l.journeys)??0}),e.jsxs("p",{children:[w(l==null?void 0:l.completed)," completed · ",w(l==null?void 0:l.abandoned)," abandoned ·"," ",w(l==null?void 0:l.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:m?`${xe(m.from)} → ${xe(m.to)}`:"Not enough data"}),e.jsx("p",{children:m?`${w(m.count)} times in this window`:"Journeys will appear here as viewers move through Memby."})]})]})]})}),e.jsxs(oe,{cols:"2",children:[e.jsx(R,{title:"What people do",intro:"Actions show total use and how many separate visits included them.",icon:"chart",tone:"info",children:e.jsx(G,{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:v.length===0?e.jsx(se,{columns:3,children:"No significant actions in this window."}):v.map(h=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:xe(h.action)}),e.jsx("span",{className:"table-sub",children:xe(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(R,{title:"Where people go",intro:"The most common steps between screens, including where quiet visits ended.",icon:"list",tone:"note",children:e.jsx(G,{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(se,{columns:2,children:"No repeated paths in this window."}):u.map((h,j)=>e.jsxs("tr",{children:[e.jsxs("td",{children:[xe(h.from)," ",e.jsx("span",{className:"route-arrow",children:"→"})," ",xe(h.to)]}),e.jsx("td",{className:"num",children:w(h.count)})]},`${h.from}:${h.to}:${j}`))})]})})})]}),e.jsx(R,{title:"Feature use",intro:"Rare and unused features are shown against Memby's major feature catalogue.",icon:"pulse",tone:"data",children:e.jsx(G,{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 b=(j==null?void 0:j.uses)??0;return e.jsxs("tr",{children:[e.jsx("td",{children:xe(h)}),e.jsx("td",{className:"num",children:w(b)}),e.jsx("td",{className:"muted nowrap",children:j?P(j.lastUsedAt):"—"}),e.jsx("td",{children:b===0?e.jsx(C,{tone:"warn",children:"not used"}):b<3?e.jsx(C,{tone:"note",children:"rare"}):e.jsx(C,{tone:"ok",children:"used"})})]},h)})})]})})}),o?null:e.jsx(R,{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 ve=s=>{const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n},As=s=>ve((s==null?void 0:s.target)||(s==null?void 0:s.screen)||(s==null?void 0:s.source)||(s==null?void 0:s.feature)),cs=s=>s.itemName?`${ve(s.itemType)} · ${s.itemName}`:s.source&&s.target?`${ve(s.source)} → ${ve(s.target)}`:As(s),pt=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"?"Started watching":"Requested",stop:"Stopped watching",start:"Started",complete:"Completed"})[s.action]??ve(s.action);function xt(s){var o;const n=(o=[...s].reverse().find(i=>i.outcome))==null?void 0:o.outcome;return n==="success"||n==="completed"?{label:ve(n),tone:"ok"}:n==="failure"||n==="cancelled"||n==="abandoned"?{label:ve(n),tone:"note"}:s.some(i=>i.action==="stop"&&i.category==="playback")?{label:"watched",tone:"ok"}:{label:"left before playback ended",tone:"warn"}}function jt(s){const n=s.reduce((o,i,a)=>(i.category==="playback"&&i.action==="request"&&o.push(a),o),[]);return n.length===0?[s]:n.map((o,i)=>s.slice(i===0?0:o,n[i+1]??s.length))}function vt(){var c,l;const{userId:s=""}=$e(),n=x.useMemo(()=>`/admin/api/journeys${Se({days:90,userId:s})}`,[s]),{data:o,error:i,loading:a}=J(n),d=((l=(c=o==null?void 0:o.users)==null?void 0:c.find(g=>g.userId===s))==null?void 0:l.username)||s,t=x.useMemo(()=>{const g=new Map;for(const v of(o==null?void 0:o.events)??[])g.set(v.journeyId,[...g.get(v.journeyId)??[],v]);return[...g.values()].map(v=>v.sort((u,m)=>u.sequence-m.sequence)).sort((v,u)=>{var m,f;return(((m=u[0])==null?void 0:m.occurredAt)??"").localeCompare(((f=v[0])==null?void 0:f.occurredAt)??"")})},[o==null?void 0:o.events]),r=t.flatMap(g=>jt(g).map((v,u)=>{var m;return{events:v,key:`${(m=g[0])==null?void 0:m.journeyId}:${u}`}}));return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:`${d}'s journeys`,intro:"Each app session is shown as the viewing journeys it contains: entry, selection, playback outcome.",icon:"journey",crumbs:e.jsx(ie,{className:"crumb",to:"/admin/journeys",children:"Journeys"})}),e.jsx(U,{message:i}),a?e.jsx(W,{}):e.jsx(R,{title:"Viewing journeys",intro:`${t.length} app session${t.length===1?"":"s"} · ${r.length} viewing journey${r.length===1?"":"s"} in the last 90 days.`,icon:"journey",tone:"info",children:e.jsx("div",{className:"visits",children:r.length===0?e.jsx("p",{className:"empty",children:"No journeys recorded for this viewer."}):r.map((g,v)=>{const u=g.events,m=u[0],f=[...u].reverse().find(j=>j.itemName||j.action==="select"||j.category==="playback"&&j.action==="request"),h=xt(u);return e.jsxs("article",{className:"visit",children:[e.jsxs("header",{children:[e.jsxs("div",{children:[e.jsx("b",{children:P(m==null?void 0:m.occurredAt)}),e.jsxs("span",{children:["Journey ",v+1," · ",u.length," recorded steps"]})]}),e.jsx(C,{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(ee,{name:"journey"}),e.jsx("span",{children:"Entered from"}),e.jsx("b",{children:As(m)})]}),e.jsxs("div",{className:"journey-answer","data-kind":"selection",children:[e.jsx(ee,{name:"play"}),e.jsx("span",{children:"Selected"}),e.jsx("b",{children:f?cs(f):"Nothing selected"})]}),e.jsxs("div",{className:"journey-answer","data-kind":"outcome",children:[e.jsx(ee,{name:h.tone==="ok"?"check":"clock"}),e.jsx("span",{children:"Outcome"}),e.jsx("b",{children:h.label})]})]}),e.jsx("ol",{className:"journey-timeline",children:u.map(j=>e.jsxs("li",{children:[e.jsx("span",{className:"timeline-dot","data-action":j.action}),e.jsxs("div",{children:[e.jsx("b",{children:pt(j)}),e.jsx("span",{children:cs(j)})]}),e.jsx("time",{children:P(j.occurredAt)})]},`${j.journeyId}:${j.sequence}`))})]},g.key)})})})]})}function ds(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function bt(){const[s,n]=x.useState(30),{data:o,error:i,loading:a}=J(`/admin/api/analytics?days=${s}`),d=(o==null?void 0:o.rows)??[];return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row."}),e.jsx(U,{message:i}),e.jsx(R,{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:t=>n(Number(t.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(W,{rows:1}):e.jsx(G,{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:d.length===0?e.jsx(se,{columns:8,children:"No events in this window."}):d.map(t=>e.jsxs("tr",{children:[e.jsx("td",{children:ds(t.rowId)}),e.jsx("td",{className:"muted",children:ds(t.rowKind)}),e.jsx("td",{className:"num",children:be(t.dwellMs)}),e.jsx("td",{className:"num",children:w(t.impressions)}),e.jsx("td",{className:"num",children:w(t.focuses)}),e.jsx("td",{className:"num",children:w(t.selects)}),e.jsx("td",{className:"num",children:Re(t.selectRate)}),e.jsx("td",{className:"num",children:w(t.viewers)})]},`${t.rowId}:${t.rowKind}`))})]})})})]})}function gt(){const[s,n]=x.useState(7),{data:o,error:i,loading:a}=J(`/admin/api/searches?days=${s}`),d=(o==null?void 0:o.terms)??[],t=(o==null?void 0:o.recent)??[],r=o==null?void 0:o.totals;return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Searches",intro:"What the household has been looking for, and what it searched just now."}),e.jsx(U,{message:i}),e.jsx(re,{tiles:[{label:"searches",value:w((r==null?void 0:r.searches)??0),icon:"search",tone:"info"},{label:"distinct queries",value:w((r==null?void 0:r.queries)??0),icon:"list",tone:"data"},{label:"viewers searching",value:w((r==null?void 0:r.viewers)??0),icon:"people",tone:"note"},{label:"history kept",value:`${(o==null?void 0:o.retentionDays)??30} days`,small:!0,icon:"clock"}]}),a?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(R,{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(G,{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:d.length===0?e.jsx(se,{columns:4,children:"Nothing searched in this window."}):d.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:P(c.lastAt)})]},c.query))})]})})}),e.jsx(R,{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(G,{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:t.length===0?e.jsx(se,{columns:3,children:"No searches in this window."}):t.map((c,l)=>e.jsxs("tr",{children:[e.jsx("td",{className:"muted nowrap",children:P(c.occurredAt)}),e.jsx("td",{children:c.username||e.jsx(C,{tone:"warn",children:c.userId||"unknown"})}),e.jsx("td",{children:c.query})]},`${c.occurredAt}:${l}`))})]})})})]})]})}function hs(s,n){if(n===0)return s>0?"new this week":"no change";const o=Math.round((s-n)/n*100);return`${o>0?"+":""}${o}% vs last week`}function ft(){const{data:s,error:n,loading:o}=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(O,{title:"Views",intro:"How often people reach Memby’s home screen. This measures app use, not playback streams."}),e.jsx(U,{message:n}),o?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(re,{tiles:[{label:hs((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:hs((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(R,{title:"Visits by day",intro:"One visit is a signed-in home-screen opening. Viewers are distinct household profiles.",icon:"chart",tone:"data",children:e.jsx(G,{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(se,{columns:3,children:"No home-screen visits yet."}):i.map(d=>e.jsxs("tr",{children:[e.jsx("td",{children:d.label}),e.jsx("td",{className:"num",children:w(d.visits)}),e.jsx("td",{className:"num",children:w(d.viewers)})]},d.label))})]})})}),e.jsx(R,{title:"Today by hour",intro:"Local New Zealand time. Use this to see when the household is opening Memby.",icon:"clock",tone:"info",children:e.jsx(G,{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(se,{columns:3,children:"No home-screen visits yet today."}):a.map(d=>e.jsxs("tr",{children:[e.jsx("td",{children:d.label}),e.jsx("td",{className:"num",children:w(d.visits)}),e.jsx("td",{className:"num",children:w(d.viewers)})]},d.label))})]})})})]})]})}const yt=s=>s.mediaType==="episode"?`${s.seriesTitle} S${String(s.seasonNumber).padStart(2,"0")}E${String(s.episodeNumber).padStart(2,"0")}`:s.title;function wt(){var a;const s=J("/admin/api/media-reports",{pollMs:15e3}),{busy:n,run:o}=Q(),i=(d,t)=>o(`${d.id}-${t}`,async()=>{await q.post(`/admin/api/media-reports/${d.id}/status`,{status:t}),await s.reload()});return e.jsxs(e.Fragment,{children:[e.jsx(O,{title:"Media reports",intro:"Viewer-reported problems and the individual replacement searches they asked Memby to start."}),s.loading?e.jsx(W,{rows:4}):e.jsx(R,{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(G,{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(d=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:yt(d)}),e.jsx("br",{}),e.jsx("span",{className:"muted",children:d.title})]}),e.jsxs("td",{children:[d.reason.replaceAll("_"," "),d.comment?e.jsxs(e.Fragment,{children:[e.jsx("br",{}),e.jsx("span",{className:"muted",children:d.comment})]}):null]}),e.jsxs("td",{children:[d.reportedByUsername,e.jsx("br",{}),e.jsx("span",{className:"muted",children:d.reportedByDevice||"Unknown device"})]}),e.jsx("td",{children:e.jsx(C,{tone:d.replacementRequested?"note":void 0,children:d.replacementRequested?d.replacementStatus||"Requested":"Not requested"})}),e.jsx("td",{children:e.jsx(C,{tone:d.status==="resolved"?"ok":d.status==="dismissed"?void 0:"warn",children:d.status})}),e.jsx("td",{className:"nowrap muted",children:P(d.createdAt)}),e.jsxs("td",{children:[e.jsx(A,{size:"sm",variant:"quiet",busy:n===`${d.id}-acknowledged`,onClick:()=>void i(d,"acknowledged"),children:"Acknowledge"})," ",e.jsx(A,{size:"sm",variant:"quiet",busy:n===`${d.id}-resolved`,onClick:()=>void i(d,"resolved"),children:"Resolve"})," ",e.jsx(A,{size:"sm",variant:"quiet",busy:n===`${d.id}-dismissed`,onClick:()=>void i(d,"dismissed"),children:"Dismiss"})]})]},d.id))})]})}):e.jsx(Z,{children:"No media problems have been reported."})})]})}const kt=s=>s==="detected"?"ok":s==="failed"?"bad":s==="no_match"?"warn":"info",Nt=s=>s==="no_match"?"no match":s,us=s=>({"live-playback":"Live playback","tracearr-next":"Next episode","tracearr-binge-prefetch":"Binge look-ahead","multi-user-demand":"Multiple viewers"}[s]??s)||"Unknown",ms=(s,n)=>s>0&&n>0?`S${String(s).padStart(2,"0")}E${String(n).padStart(2,"0")}`:"Episode";function St(){var h,j,b,k;const s=J("/admin/api/credits?limit=150",{pollMs:15e3}),{wrap:n}=Y(),{busy:o,run:i}=Q(),[a,d]=x.useState(),[t,r]=x.useState(!1);x.useEffect(()=>{!t&&s.data&&d(s.data.settings)},[s.data,t]);const c=p=>{d(y=>y&&{...y,...p}),r(!0)},l=()=>{a&&i("save",async()=>{const p=await n(()=>q.put("/admin/api/credits",a),"Credits scanning settings saved.");p&&(s.set(p),d(p.settings),r(!1))})},g=((h=s.data)==null?void 0:h.history)??[],v=((j=s.data)==null?void 0:j.pending)??[],u=g.filter(p=>p.outcome==="detected").length,m=g.filter(p=>p.outcome==="no_match").length,f=g.filter(p=>p.outcome==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(O,{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(U,{message:s.error}),s.loading||!a?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[(b=s.data)!=null&&b.enabled?null:e.jsx(ue,{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(re,{tiles:[{label:"Waiting candidates",value:w((k=s.data)==null?void 0:k.queueDepth),icon:"clock",tone:v.length?"info":void 0},{label:"Detected in this history",value:w(u),icon:"check",tone:"ok"},{label:"No match",value:w(m),icon:"search",tone:m?"warn":void 0},{label:"Failed",value:w(f),icon:"alert",tone:f?"bad":void 0}]}),e.jsxs(oe,{cols:"wide",children:[e.jsx(R,{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(A,{variant:"primary",icon:"check",busy:o==="save",disabled:!t,onClick:l,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(R,{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(R,{title:"Waiting candidates",intro:"The exact worker order after marker checks and retry cooldowns. A refresh may replace this list as household viewing changes.",icon:"list",tone:"info",children:e.jsx(G,{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:v.length===0?e.jsx(se,{columns:6,children:"No episodes are waiting to be scanned."}):v.map(p=>e.jsxs("tr",{children:[e.jsx("td",{children:ms(p.season,p.episode)}),e.jsx("td",{children:e.jsx(C,{tone:"info",children:us(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:P(p.lastViewed),children:pe(p.lastViewed)}),e.jsx("td",{className:"mono muted",children:p.itemId})]},p.itemId))})]})})}),e.jsx(R,{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(G,{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:g.length===0?e.jsx(se,{columns:7,children:"No credits scans have completed yet."}):g.map(p=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(p.finishedAt),children:pe(p.finishedAt)}),e.jsxs("td",{children:[e.jsx("b",{children:p.seriesName||p.itemName||p.itemId}),e.jsxs("span",{className:"table-sub",children:[ms(p.season,p.episode),p.itemName&&p.seriesName?` · ${p.itemName}`:""]})]}),e.jsxs("td",{children:[e.jsx(C,{tone:"info",children:us(p.reason)}),e.jsxs("span",{className:"table-sub",children:["priority ",p.priority]})]}),e.jsxs("td",{children:[e.jsx(C,{tone:kt(p.outcome),children:Nt(p.outcome)}),p.error?e.jsx("span",{className:"table-sub",children:p.error}):null]}),e.jsx("td",{className:"nowrap",children:p.markerMs>0?be(p.markerMs):"—"}),e.jsxs("td",{className:"muted",children:[p.method||"visual",p.confidence>0?` · ${Re(p.confidence)}`:"",p.frames>0?` · ${p.frames} frames`:""]}),e.jsx("td",{className:"num muted",children:be(p.durationMs)})]},p.id))})]})})})]})]})}function Ct(){return e.jsx(O,{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 Mt(){return e.jsx(Ls,{children:e.jsx(Zs,{children:e.jsx(en,{children:e.jsx(ln,{children:e.jsx(Ds,{children:e.jsxs(V,{path:"/admin",element:e.jsx(an,{}),children:[e.jsx(V,{index:!0,element:e.jsx(on,{})}),e.jsx(V,{path:"activity",element:e.jsx(dn,{})}),e.jsx(V,{path:"accounts",element:e.jsx(hn,{})}),e.jsx(V,{path:"accounts/:userId",element:e.jsx(un,{})}),e.jsx(V,{path:"accounts/:userId/settings",element:e.jsx(bn,{})}),e.jsx(V,{path:"clients",element:e.jsx(fn,{})}),e.jsx(V,{path:"logins",element:e.jsx(wn,{})}),e.jsx(V,{path:"devices/:deviceId",element:e.jsx(Cn,{})}),e.jsx(V,{path:"library",element:e.jsx(Mn,{})}),e.jsx(V,{path:"ratings",element:e.jsx(An,{})}),e.jsx(V,{path:"requests",element:e.jsx(Rn,{})}),e.jsx(V,{path:"recommendations",element:e.jsx(In,{})}),e.jsx(V,{path:"inspector",element:e.jsx(Ln,{})}),e.jsx(V,{path:"hero",element:e.jsx(Pn,{})}),e.jsx(V,{path:"features",element:e.jsx(Un,{})}),e.jsx(V,{path:"playback",element:e.jsx(Vn,{})}),e.jsx(V,{path:"subtitles",element:e.jsx(Bn,{})}),e.jsx(V,{path:"credits",element:e.jsx(St,{})}),e.jsx(V,{path:"updates",element:e.jsx(_n,{})}),e.jsx(V,{path:"tasks",element:e.jsx(Gn,{})}),e.jsx(V,{path:"integrations",element:e.jsx(Jn,{})}),e.jsx(V,{path:"maintenance",element:e.jsx(nt,{})}),e.jsx(V,{path:"settings",element:e.jsx(it,{})}),e.jsx(V,{path:"imports",element:e.jsx(at,{})}),e.jsx(V,{path:"logs",element:e.jsx(ut,{})}),e.jsx(V,{path:"journeys",element:e.jsx(mt,{})}),e.jsx(V,{path:"journeys/:userId",element:e.jsx(vt,{})}),e.jsx(V,{path:"views",element:e.jsx(ft,{})}),e.jsx(V,{path:"engagement",element:e.jsx(bt,{})}),e.jsx(V,{path:"searches",element:e.jsx(gt,{})}),e.jsx(V,{path:"media-reports",element:e.jsx(wt,{})}),e.jsx(V,{path:"overview",element:e.jsx(qs,{to:"/admin",replace:!0})}),e.jsx(V,{path:"*",element:e.jsx(Ct,{})})]})})})})})})}const Rs=document.getElementById("root");if(!Rs)throw new Error("the console has no root element to render into");vs(Rs).render(e.jsx(x.StrictMode,{children:e.jsx(Mt,{})})); diff --git a/admin-ui/dist/assets/index-Cg_z5PGS.css b/admin-ui/dist/assets/index-Cg_z5PGS.css deleted file mode 100644 index aeda15e..0000000 --- a/admin-ui/dist/assets/index-Cg_z5PGS.css +++ /dev/null @@ -1 +0,0 @@ -@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2) format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2) format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2) format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-greek-wght-normal-CkhJZR-_.woff2) format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2) format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2) format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-latin-wght-normal-Dx4kXJAl.woff2) format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{color-scheme:dark;--oled: #000;--bg: #080b10;--rail-bg: var(--oled);--top-bg: rgba(1, 4, 9, .94);--surface: #0d1117;--surface-lift: #161b22;--surface-hi: #21262d;--line: #30363d;--line-soft: #21262d;--text: #f0f6fc;--muted: #b1bac4;--quiet: #7d8590;--accent: #2ea043;--accent-ink: #56d364;--accent-wash: rgba(46, 160, 67, .16);--danger: #e5534b;--danger-ink: #ff9b94;--danger-wash: rgba(229, 83, 75, .13);--warn: #e0ad4e;--warn-ink: #f2cb78;--warn-wash: rgba(239, 196, 107, .13);--info: #4f9cd8;--info-ink: #93cbef;--info-wash: rgba(79, 156, 216, .14);--note: #a07ce8;--note-ink: #bda1f5;--note-wash: rgba(160, 124, 232, .14);--data: #3fc2b6;--data-ink: #6fdcd0;--data-wash: rgba(63, 194, 182, .13);--rail: 236px;--safe-top: env(safe-area-inset-top, 0px);--safe-right: env(safe-area-inset-right, 0px);--safe-bottom: env(safe-area-inset-bottom, 0px);--safe-left: env(safe-area-inset-left, 0px);--top: calc(58px + var(--safe-top));--radius: 8px;--radius-sm: 6px;--radius-xs: 4px;--sans: "Inter Variable", Inter, "Segoe UI", sans-serif;--mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:radial-gradient(circle at 72% -20%,rgba(63,194,182,.045),transparent 38rem),var(--bg);color:var(--text);font:16.5px/1.55 var(--sans);font-weight:450;letter-spacing:0;-webkit-font-smoothing:antialiased;min-width:320px;min-height:100dvh;background:var(--bg);-webkit-tap-highlight-color:transparent}a{color:var(--accent-ink)}@media (prefers-reduced-motion: reduce){*,*:before,*:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}}.skip{position:absolute;left:-9999px;top:8px;z-index:60;padding:8px 14px;border-radius:var(--radius-sm);background:var(--surface-lift);color:var(--text);text-decoration:none}.skip:focus{left:8px}:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-xs)}.topbar{position:fixed;inset:0 0 auto 0;z-index:30;height:var(--top);display:flex;align-items:center;gap:12px;padding:var(--safe-top) max(clamp(14px,2vw,22px),var(--safe-right)) 0 var(--safe-left);background:var(--top-bg);border-bottom:1px solid var(--line);box-shadow:0 1px #f0f6fc05,0 8px 24px #0003;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}.topbar-brand{display:flex;align-items:center;gap:11px;flex:0 0 var(--rail);height:100%;padding:0 22px;color:var(--text);text-decoration:none}.brand-mark{display:grid;place-items:center;width:30px;height:30px;flex:0 0 30px;border-radius:var(--radius-sm);background:var(--accent);color:#fff;font-size:16px;font-weight:800;letter-spacing:-.06em}.brand-word{font-size:17px;font-weight:650;letter-spacing:-.01em;white-space:nowrap}.topbar-spacer{flex:1 1 auto;min-width:0}.topbar-tools{display:flex;align-items:center;gap:10px;min-width:0}.topbar .omni{position:absolute;left:50%;transform:translate(-50%);width:min(820px,calc(100vw - var(--rail) - 380px))}.topbar-version{font:11.5px/1 var(--mono);color:var(--quiet);white-space:nowrap}.topbar-status{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;padding:0;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:inherit;cursor:pointer;transition:background .16s ease,border-color .16s ease,color .16s ease}.topbar-status:hover:not(:disabled),.topbar-status:focus-visible{background:var(--surface-lift)}.topbar-status:disabled{cursor:default;opacity:1}.topbar-status .dot{width:9px;height:9px;border-radius:50%;background:var(--quiet);box-shadow:0 0 0 3px #7d85901f}.topbar-status[data-tone=ok] .dot{background:var(--accent-ink);box-shadow:0 0 0 3px var(--accent-wash),0 0 9px #56d36447}.topbar-status[data-tone=bad] .dot{background:var(--danger);box-shadow:0 0 0 3px var(--danger-wash)}.account-menu{position:relative;display:flex;align-items:center;height:38px;flex:0 0 auto}.account-trigger{max-width:190px;height:38px;padding:0 9px 0 5px;border-color:var(--line);background:var(--surface);color:var(--muted)}.account-trigger:hover:not(:disabled),.account-menu[data-open=true] .account-trigger{border-color:#56d36480;background:var(--surface-hi);color:var(--text)}.account-avatar{display:grid;place-items:center;width:27px;height:27px;flex:0 0 27px;border:1px solid rgba(86,211,100,.36);border-radius:50%;background:var(--accent-wash);color:var(--accent-ink);font-size:11px;font-weight:750}.account-name{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:12.5px}.account-caret{width:12px;height:12px;transition:transform .16s ease}.account-menu[data-open=true] .account-caret{transform:rotate(180deg)}.account-panel{position:absolute;top:calc(100% + 8px);right:0;width:min(280px,calc(100vw - 24px));padding:7px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 18px 44px #0000008c;z-index:40}.account-identity{display:flex;align-items:center;gap:11px;min-width:0;padding:9px 10px 12px;border-bottom:1px solid var(--line-soft)}.account-avatar-large{width:34px;height:34px;flex-basis:34px;font-size:13px}.account-identity span:last-child{min-width:0}.account-identity small,.account-identity b{display:block}.account-identity small{color:var(--quiet);font-size:11px}.account-identity b{overflow:hidden;text-overflow:ellipsis;font-size:13.5px}.account-panel>a,.account-panel form button{display:flex;align-items:center;gap:9px;justify-content:flex-start;width:100%;height:38px;padding:0 10px;border-radius:8px;color:var(--muted);font-size:13px;text-decoration:none}.account-panel>a:hover{background:var(--surface-hi);color:var(--text)}.account-panel>a{margin-top:6px}.account-panel form{margin-top:2px}.account-panel form button{justify-content:flex-start;width:100%;height:38px;border-color:transparent;background:transparent;color:var(--muted)}.account-panel form button:hover:not(:disabled){background:var(--surface-hi);color:var(--text)}@media (max-width: 900px){.topbar-version{display:none}}.omni{position:relative;width:100%}.omni-input{display:flex;align-items:center;gap:8px;width:100%;height:38px;padding:0 10px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface)}.omni[data-open=true] .omni-input{border-color:var(--accent)}.omni-input input[type=search]{flex:1 1 auto;min-width:0;height:100%;padding:0;border:0;border-radius:0;background:none;color:var(--text);font:inherit;font-size:14px;outline:none}.omni-input input::placeholder{color:var(--quiet)}.omni-input .ico{color:var(--quiet);flex:0 0 15px}.omni-key{flex:0 0 auto;padding:2px 6px;border:1px solid var(--line);border-radius:4px;font:10.5px/1.3 var(--mono);color:var(--quiet)}.omni-panel{position:absolute;top:calc(100% + 6px);right:auto;left:0;width:100%;max-height:min(50vh,420px);overflow-y:auto;padding:6px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);box-shadow:0 18px 44px #0000008c;z-index:40}.omni-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--radius-sm);color:var(--text);text-decoration:none}.omni-item .ico{color:var(--quiet);flex:0 0 16px}.omni-item.on,.omni-item:hover{background:var(--surface-hi)}.omni-item b{font-size:13.5px;font-weight:600}.omni-item small{display:block;color:var(--quiet);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-item span{flex:1 1 auto;min-width:0}.omni-group{flex:0 0 auto;color:var(--quiet);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase}.bell{position:relative;display:flex;align-items:center;height:38px}.bell-button{position:relative;display:grid;place-items:center;width:38px;height:38px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--muted);cursor:pointer}.bell-button:hover{background:var(--surface-hi);color:var(--text)}.bell[data-open=true] .bell-button{border-color:var(--accent);color:var(--text)}.bell-badge{position:absolute;top:-6px;right:-6px;min-width:17px;height:17px;padding:0 4px;border-radius:9px;background:var(--danger);color:#fff;font:700 10.5px/17px var(--sans);text-align:center}.bell-panel{position:absolute;top:calc(100% + 8px);right:0;width:min(420px,92vw);border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);box-shadow:0 18px 44px #0000008c;z-index:40;overflow:hidden}.bell-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--line)}.bell-head b{font-size:13px}.bell-list{max-height:min(58vh,460px);overflow-y:auto}.bell-item{display:flex;gap:11px;padding:11px 14px;border-bottom:1px solid var(--line-soft);color:var(--text);text-decoration:none}.bell-item:last-child{border-bottom:0}.bell-item:hover{background:var(--surface-lift)}.bell-item[data-unread=true]{box-shadow:inset 2px 0 0 var(--accent)}.bell-item .glyph{flex:0 0 auto}.bell-body{flex:1 1 auto;min-width:0}.bell-body b{display:block;font-size:13px;font-weight:600}.bell-body p{margin:2px 0 0;color:var(--muted);font-size:12.5px;overflow-wrap:anywhere}.bell-body time{display:block;margin-top:3px;color:var(--quiet);font-size:11px}.bell-foot{padding:9px 14px;border-top:1px solid var(--line);text-align:center}.bell-foot a{font-size:12.5px;text-decoration:none}.rail{position:fixed;top:var(--top);bottom:0;left:0;width:var(--rail);padding:12px 12px max(20px,var(--safe-bottom));overflow-y:auto;background:var(--rail-bg);border-right:1px solid var(--line);z-index:20}.rail-group+.rail-group{margin-top:4px}.rail-group:first-child{margin-bottom:9px;padding-bottom:9px;border-bottom:1px solid var(--line-soft)}.rail-head{display:flex;align-items:center;gap:6px;width:100%;padding:9px 10px 5px;border:0;background:none;color:var(--quiet);font:600 10.5px/1 var(--sans);letter-spacing:.1em;text-transform:uppercase;cursor:pointer}.rail-head .caret{margin-left:auto;transition:transform .12s ease}.rail-head[aria-expanded=false] .caret{transform:rotate(-90deg)}.rail-head-static{cursor:default;color:var(--muted)}.rail a{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:var(--radius-sm);color:var(--muted);font-size:13.5px;text-decoration:none}.rail a:hover{background:var(--surface);color:var(--text)}.rail a[aria-current=page]{background:var(--accent-wash);color:var(--accent-ink);font-weight:600}.rail a .ico{flex:0 0 17px;opacity:.85}.rail a[aria-current=page] .ico{opacity:1}.rail-badge{margin-left:auto;min-width:18px;padding:0 5px;border-radius:9px;background:var(--danger);color:#fff;font:700 10.5px/17px var(--sans);text-align:center}.page{width:min(calc(100vw - var(--rail)),1920px);min-width:0;margin:var(--top) 0 0 calc(var(--rail) + max(0px,(100vw - var(--rail) - 1920px) / 2));padding:clamp(24px,2.4vw,44px) clamp(22px,3vw,56px) 80px}.page-head{margin-bottom:20px}.page-head h1{margin:0;font-size:22px;font-weight:650;letter-spacing:-.02em}.page-head p{margin:5px 0 0;max-width:68ch;color:var(--muted);font-size:13.5px}.page-head-row{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap}.page-head-title{display:flex;align-items:flex-start;gap:12px;min-width:0}.page-head-icon{display:grid;place-items:center;flex:0 0 40px;width:40px;height:40px;border:1px solid var(--line);border-radius:10px;background:var(--accent-wash);color:var(--accent-ink)}.page-head-icon .ico{width:20px;height:20px}.page-head-text{min-width:0}.page-head-row .page-head-actions{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}.crumbs{display:flex;align-items:center;gap:6px;margin-bottom:8px;color:var(--quiet);font-size:12px}.crumbs a{color:var(--muted);text-decoration:none}.crumbs a:hover{color:var(--text)}@media (max-width: 1400px){:root{--rail: 0px}.rail{transform:translate(-100%);transition:transform .2s cubic-bezier(.22,1,.36,1);width:min(320px,calc(100vw - 72px));padding-right:14px;padding-left:max(14px,var(--safe-left));background:#080c12fa;box-shadow:none}.rail[data-open=true]{transform:none;box-shadow:24px 0 80px #00000094}.topbar-brand{flex:0 0 auto;padding:0 4px 0 12px}.page{margin-left:0}}.rail-scrim{display:none}@media (max-width: 1400px){.rail-scrim{position:fixed;inset:var(--top) 0 0 0;z-index:19;display:block;width:auto;height:auto;padding:0;border:0;border-radius:0;background:#01040994;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);animation:rail-scrim-in .16s ease-out}.rail-scrim:hover:not(:disabled){border:0;background:#01040994}}@keyframes rail-scrim-in{0%{opacity:0}}.rail-toggle{display:none;width:34px;height:34px;margin-left:8px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--muted);cursor:pointer}@media (max-width: 1400px){.rail-toggle{display:grid;place-items:center}}.banner{display:flex;align-items:center;gap:10px;margin-bottom:16px;padding:11px 14px;border:1px solid var(--danger);border-radius:var(--radius-sm);background:var(--danger-wash);color:var(--danger-ink);font-size:13.5px}.banner button{margin-left:auto;border:0;background:none;color:inherit;cursor:pointer;font:inherit}.card{padding:16px 18px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.card+.card,.grid+.card,.card+.grid,.grid+.grid{margin-top:16px}.card-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}.card-head h2{margin:0;font-size:14.5px;font-weight:620;letter-spacing:-.01em}.card-head p{margin:2px 0 0;color:var(--muted);font-size:12.5px}.card-head-text{min-width:0}.card-head-actions{margin-left:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.card-foot{margin-top:14px;padding-top:13px;border-top:1px solid var(--line-soft);display:flex;align-items:center;gap:8px;flex-wrap:wrap}.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.grid[data-cols="2"]{grid-template-columns:repeat(auto-fit,minmax(400px,1fr))}.grid[data-cols=wide]{grid-template-columns:minmax(0,2fr) minmax(300px,1fr)}@media (max-width: 1100px){.grid[data-cols=wide]{grid-template-columns:1fr}}.tiles{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));margin-bottom:16px}.tile{padding:13px 15px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.tile .glyph{margin:0 0 9px}.tile b{display:block;font-size:24px;font-weight:650;letter-spacing:-.03em;line-height:1.15}.tile b.small{font-size:14.5px;font-weight:600;letter-spacing:-.01em;line-height:1.4}.tile span{display:block;margin-top:3px;color:var(--muted);font-size:12px}a.tile{color:inherit;text-decoration:none}a.tile:hover{border-color:var(--accent);background:var(--surface-lift)}.ico{display:block;width:1em;height:1em;font-size:17px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:0 0 auto}.glyph{display:grid;place-items:center;width:30px;height:30px;flex:0 0 30px;border-radius:var(--radius-sm);background:var(--surface-hi);color:var(--muted)}.tile .glyph .ico{width:18px;height:18px;margin:auto}.loading-page{display:grid;gap:20px}.loading-heading{display:grid;gap:10px;max-width:48rem}.loading-heading .skeleton:first-child{height:34px;width:28%}.loading-heading .skeleton:last-child{height:18px;width:72%}.loading-tiles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.loading-tiles .skeleton{height:132px}.loading-card{display:grid;gap:12px;min-height:180px;padding:22px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.loading-card .skeleton:first-child{height:20px;width:32%}.loading-card .skeleton:not(:first-child){height:14px}@media (max-width: 820px){.loading-tiles{grid-template-columns:repeat(2,minmax(0,1fr))}}.glyph .ico{display:block}.glyph[data-tone=ok]{background:var(--accent-wash);color:var(--accent-ink)}.glyph[data-tone=warn]{background:var(--warn-wash);color:var(--warn-ink)}.glyph[data-tone=bad]{background:var(--danger-wash);color:var(--danger-ink)}.glyph[data-tone=info]{background:var(--info-wash);color:var(--info-ink)}.glyph[data-tone=note]{background:var(--note-wash);color:var(--note-ink)}.glyph[data-tone=data]{background:var(--data-wash);color:var(--data-ink)}.tag{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border:1px solid var(--line);border-radius:999px;background:var(--surface-lift);color:var(--muted);font-size:11.5px;font-weight:600;white-space:nowrap}.tag[data-tone]:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}.tag[data-tone=ok]{border-color:#52b54b59;background:var(--accent-wash);color:var(--accent-ink)}.tag[data-tone=warn]{border-color:#efc46b59;background:var(--warn-wash);color:var(--warn-ink)}.tag[data-tone=bad]{border-color:#e5534b59;background:var(--danger-wash);color:var(--danger-ink)}.tag[data-tone=info]{border-color:#4f9cd859;background:var(--info-wash);color:var(--info-ink)}.tag[data-tone=note]{border-color:#a07ce859;background:var(--note-wash);color:var(--note-ink)}.tag[data-tone=data]{border-color:#3fc2b659;background:var(--data-wash);color:var(--data-ink)}.chip{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border:1px solid var(--line);border-radius:var(--radius-xs);background:var(--surface-lift);color:var(--muted);font:11.5px/1.5 var(--mono);white-space:nowrap}.chip[data-tone=ok]{color:var(--accent-ink)}.chip[data-tone=warn]{color:var(--warn-ink)}.chip[data-tone=bad]{color:var(--danger-ink)}.chip[data-tone=info]{color:var(--info-ink)}.chip[data-tone=note]{color:var(--note-ink)}.chip[data-tone=data]{color:var(--data-ink)}.chips{display:flex;flex-wrap:wrap;gap:6px}.table-wrap{overflow-x:auto;overscroll-behavior-inline:contain;-webkit-overflow-scrolling:touch;margin:0 -18px -16px;padding:0 18px 16px}table{width:100%;min-width:max-content;border-collapse:collapse;font-size:13px}@media (max-width: 1400px){.topbar{display:flex;gap:10px;height:var(--top);padding:var(--safe-top) max(16px,var(--safe-right)) 0 max(16px,var(--safe-left))}.topbar-brand{padding:0}.rail-toggle{margin-left:0}.topbar-spacer{display:none}.topbar .omni{position:relative;left:auto;transform:none;flex:1 1 auto;width:auto;max-width:640px;min-width:0}.omni-key,.topbar-version{display:none}.topbar-tools{display:contents}.bell-button,.rail-toggle{width:40px;height:40px}.topbar-status{width:40px;flex-basis:40px;min-width:40px;height:40px;border:1px solid var(--line);background:var(--surface)}.omni-input,.bell,.bell-button,.account-menu,.account-trigger{height:40px}.page{width:100%;padding:clamp(24px,3vw,34px) max(24px,calc(var(--safe-right) + 10px)) max(64px,calc(var(--safe-bottom) + 36px)) max(24px,calc(var(--safe-left) + 10px))}.table-wrap{margin:0 -18px -16px;padding:0 18px 18px;scrollbar-gutter:stable}table{font-size:13px}th,td{padding-right:16px}}@media (max-width: 1100px){.account-trigger{width:40px;padding:0}.account-name,.account-caret{display:none}}@media (max-width: 820px){.brand-word{display:none}.topbar-status{display:grid;place-items:center;width:40px;padding:0}.page{padding:22px max(16px,var(--safe-right)) max(56px,calc(var(--safe-bottom) + 28px)) max(16px,var(--safe-left))}.grid,.grid[data-cols="2"],.grid[data-cols=wide],.grid.two{grid-template-columns:minmax(0,1fr)}.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}.table-wrap{margin-right:-16px;margin-left:-16px;padding-right:16px;padding-left:16px}}@media (max-width: 680px){:root{--top: calc(60px + var(--safe-top))}.topbar{align-items:center;flex-wrap:nowrap;gap:clamp(4px,1.5vw,8px);padding:calc(var(--safe-top) + 8px) max(8px,var(--safe-right)) 8px max(8px,var(--safe-left))}.rail-toggle{order:1}.topbar-brand{order:2;height:40px;flex:0 0 auto}.topbar .omni{order:3;flex:1 1 96px;width:0;min-width:0;max-width:none}.topbar-status{order:4;margin-left:0}.bell{order:5}.account-menu{order:6}.brand-mark{width:30px;height:30px;flex-basis:30px}.account-panel,.bell-panel{position:fixed;top:calc(var(--top) + 8px)}.account-panel{right:max(12px,var(--safe-right))}}@media (max-width: 370px){.tiles{grid-template-columns:minmax(0,1fr)}}th{padding:0 12px 8px 0;border-bottom:1px solid var(--line);color:var(--quiet);font-weight:600;font-size:11px;letter-spacing:.07em;text-transform:uppercase;text-align:left;white-space:nowrap}td{padding:9px 12px 9px 0;border-bottom:1px solid var(--line-soft);vertical-align:middle}tr:last-child td{border-bottom:0}tbody tr:hover td{background:var(--surface-lift)}th:last-child,td:last-child{padding-right:0}td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}td.mono{font:12px/1.5 var(--mono);color:var(--muted)}td.nowrap,th.nowrap{white-space:nowrap}td.muted{color:var(--muted)}th button{display:inline-flex;align-items:center;gap:4px;border:0;padding:0;background:none;color:inherit;font:inherit;letter-spacing:inherit;text-transform:inherit;cursor:pointer}th button:hover{color:var(--text)}th[aria-sort] button{color:var(--accent-ink)}.table-row-link{color:var(--text);text-decoration:none;font-weight:600}.table-row-link:hover{color:var(--accent-ink)}.list{display:flex;flex-direction:column;gap:1px}.list-item{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--line-soft)}.list-item:last-child{border-bottom:0}.list-item .list-body{flex:1 1 auto;min-width:0}.list-item b{display:block;font-size:13.5px;font-weight:600}.list-item p{margin:2px 0 0;color:var(--muted);font-size:12.5px}.list-item .list-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}button,.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:32px;padding:0 13px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-lift);color:var(--text);font:600 13px/1 var(--sans);text-decoration:none;cursor:pointer;white-space:nowrap}button:hover:not(:disabled),.btn:hover{background:var(--surface-hi);border-color:#2c3542}button:disabled{opacity:.45;cursor:not-allowed}button[data-variant=primary]{border-color:var(--accent);background:var(--accent);color:#06240a}button[data-variant=primary]:hover:not(:disabled){background:var(--accent-ink);border-color:var(--accent-ink)}button[data-variant=danger]{border-color:#e5534b66;background:var(--danger-wash);color:var(--danger-ink)}button[data-variant=danger]:hover:not(:disabled){background:#e5534b38}button[data-variant=quiet]{border-color:transparent;background:none;color:var(--muted)}button[data-variant=quiet]:hover:not(:disabled){background:var(--surface-lift);color:var(--text)}button[data-size=sm]{height:26px;padding:0 9px;font-size:12px}.field{display:block;min-width:0}.field>span{display:block;margin-bottom:5px;color:var(--muted);font-size:12px;font-weight:600}.field>small{display:block;margin-top:5px;color:var(--quiet);font-size:11.5px}input[type=text],input[type=password],input[type=search],input[type=number],input[type=date],input[type=time],input[type=datetime-local],input[type=url],select,textarea{width:100%;height:32px;padding:0 10px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font:inherit;font-size:13px}textarea{height:auto;min-height:74px;padding:8px 10px;resize:vertical}input:focus,select:focus,textarea:focus{border-color:var(--accent);outline:none}input::placeholder,textarea::placeholder{color:var(--quiet)}select{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding-right:26px;background-image:linear-gradient(45deg,transparent 50%,var(--quiet) 50%),linear-gradient(135deg,var(--quiet) 50%,transparent 50%);background-position:calc(100% - 14px) 14px,calc(100% - 9px) 14px;background-size:5px 5px;background-repeat:no-repeat}.fields{display:grid;gap:13px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}.field-row{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap}.check{display:flex;align-items:flex-start;gap:11px;padding:10px 0;cursor:pointer}.check input{position:absolute;opacity:0;pointer-events:none}.check .switch{position:relative;width:34px;height:20px;flex:0 0 34px;margin-top:1px;border-radius:999px;background:var(--surface-hi);border:1px solid var(--line);transition:background .12s ease,border-color .12s ease}.check .switch:after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--muted);transition:transform .12s ease,background .12s ease}.check input:checked+.switch{background:var(--accent-wash);border-color:var(--accent)}.check input:checked+.switch:after{transform:translate(14px);background:var(--accent)}.check input:focus-visible+.switch{outline:2px solid var(--accent);outline-offset:2px}.check input:disabled+.switch{opacity:.45}.check-body b{display:block;font-size:13.5px;font-weight:600}.check-body p{margin:2px 0 0;color:var(--muted);font-size:12.5px}.segments{display:inline-flex;padding:2px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface)}.segments button{height:26px;border:0;background:none;color:var(--muted);font-size:12.5px;font-weight:600}.segments button[aria-pressed=true]{background:var(--surface-hi);color:var(--text)}.filters{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap;margin-bottom:14px;padding:13px 15px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.filters .field{flex:1 1 160px;max-width:260px}.filters .field.grow{flex:2 1 240px;max-width:none}.filters .filter-actions{margin-left:auto;display:flex;align-items:center;gap:8px}.filter-summary{color:var(--quiet);font-size:12px;white-space:nowrap}.bars{display:flex;align-items:flex-end;gap:3px;height:92px;padding-top:6px}.bar{position:relative;flex:1 1 0;min-width:3px;border-radius:2px 2px 0 0;background:var(--accent-wash);border-top:2px solid var(--accent);min-height:2px}.bar[data-tone=bad]{background:var(--danger-wash);border-top-color:var(--danger)}.bar[data-empty=true]{background:var(--line-soft);border-top-color:var(--line)}.bars-axis{display:flex;justify-content:space-between;margin-top:6px;color:var(--quiet);font-size:11px}.meter{height:6px;border-radius:3px;background:var(--surface-hi);overflow:hidden}.meter>div{height:100%;background:var(--accent)}.meter[data-tone=warn]>div{background:var(--warn)}.meter[data-tone=bad]>div{background:var(--danger)}.logview{height:62vh;min-height:320px;overflow:auto;border:1px solid var(--line);border-radius:var(--radius-sm);background:#080a0e;font:12px/1.6 var(--mono);contain:layout paint style}.loghead{position:sticky;top:0;z-index:1;display:grid;grid-template-columns:150px 46px minmax(180px,1fr) minmax(240px,2fr);gap:12px;padding:7px 12px;border-bottom:1px solid var(--line);background:#10131a;color:var(--quiet);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;min-width:760px}.logbody{position:relative;min-width:760px}.logline{display:flex;gap:12px;padding:3px 12px;border-bottom:1px solid rgba(255,255,255,.03);white-space:pre-wrap;overflow-wrap:anywhere}.logline[data-virtual=true]{position:absolute;top:0;right:0;left:0;display:grid;grid-template-columns:150px 46px minmax(180px,1fr) minmax(240px,2fr);align-items:center;height:48px;padding-top:4px;padding-bottom:4px;white-space:nowrap;contain:strict}.logline:hover{background:#ffffff08}.logline time{flex:0 0 auto;color:var(--quiet)}.logline time small{display:block;color:var(--muted);font-size:10px}.logline .lvl{flex:0 0 46px;font-weight:700}.logline[data-level=ERROR] .lvl{color:var(--danger-ink)}.logline[data-level=WARN] .lvl{color:var(--warn-ink)}.logline[data-level=INFO] .lvl{color:var(--info-ink)}.logline[data-level=DEBUG] .lvl{color:var(--quiet)}.logline .msg{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}.logline .attrs{color:var(--quiet)}@media (min-width: 900px){.logline{display:grid;grid-template-columns:150px 46px minmax(180px,1fr) minmax(240px,2fr)}}.logline .attrs b{color:var(--muted);font-weight:400}.logattrs-button{display:block;width:100%;min-width:0;height:28px;padding:0;overflow:hidden;border:0;background:transparent;color:var(--quiet);font:inherit;text-align:left;text-overflow:ellipsis;white-space:nowrap}.logattrs-button:hover:not(:disabled){border:0;background:transparent;color:var(--text)}.logattrs-button:focus-visible{outline:1px solid var(--accent);outline-offset:2px}.log-inspector{margin-top:12px;padding:12px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface)}.log-inspector-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.log-inspector-head>div{min-width:0}.log-inspector-head b,.log-inspector-head span{display:block;overflow-wrap:anywhere}.log-inspector-head span{margin-top:3px;color:var(--quiet);font:11px/1.5 var(--mono)}.log-inspector dl{display:grid;grid-template-columns:minmax(130px,max-content) minmax(0,1fr);gap:5px 14px;margin:12px 0 0;padding-top:10px;border-top:1px solid var(--line);font:12px/1.5 var(--mono)}.log-inspector dt{color:var(--muted)}.log-inspector dd{min-width:0;margin:0;overflow-wrap:anywhere}.logdetails{min-width:0}.logdetails summary{cursor:pointer;list-style:none;overflow-wrap:anywhere}.logdetails summary::-webkit-details-marker{display:none}.logdetails summary:before{content:"›";display:inline-block;margin-right:6px;color:var(--accent)}.logdetails[open] summary:before{transform:rotate(90deg)}.logdetails dl{display:grid;grid-template-columns:minmax(130px,max-content) minmax(0,1fr);gap:4px 12px;margin:7px 0 2px;padding:8px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#ffffff06}.logdetails dt{color:var(--muted)}.logdetails dd{min-width:0;margin:0;color:var(--text);overflow-wrap:anywhere}pre.code{margin:0;padding:12px 14px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#080a0e;color:var(--muted);font:12px/1.6 var(--mono);overflow-x:auto}.empty{margin:0;padding:22px 0;color:var(--quiet);font-size:13px;text-align:center}.muted{color:var(--muted)}.quiet{color:var(--quiet)}.mono{font-family:var(--mono);font-size:12px}.note{margin:0;padding:10px 13px;border:1px solid var(--line);border-left:2px solid var(--info);border-radius:var(--radius-xs);background:var(--surface-lift);color:var(--muted);font-size:12.5px}.note[data-tone=warn]{border-left-color:var(--warn)}.note[data-tone=bad]{border-left-color:var(--danger)}.note[data-tone=ok]{border-left-color:var(--accent)}.stack{display:flex;flex-direction:column;gap:12px}.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.row.tight{gap:6px}.spacer{flex:1 1 auto}.spinner{width:14px;height:14px;border:2px solid var(--line);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.skeleton{border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--surface) 25%,var(--surface-lift) 50%,var(--surface) 75%);background-size:300% 100%;animation:shimmer 1.3s ease-in-out infinite}@keyframes shimmer{to{background-position:-300% 0}}.toasts{position:fixed;right:18px;bottom:18px;z-index:50;display:flex;flex-direction:column;gap:8px;max-width:min(420px,90vw)}.toast{display:flex;align-items:flex-start;gap:10px;padding:11px 14px;border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--radius-sm);background:var(--surface-lift);box-shadow:0 14px 34px #00000080;font-size:13px;animation:toast-in .14s ease}.toast[data-tone=bad]{border-left-color:var(--danger)}.toast[data-tone=warn]{border-left-color:var(--warn)}.toast button{margin-left:auto;height:auto;padding:0;border:0;background:none;color:var(--quiet)}@keyframes toast-in{0%{opacity:0;transform:translateY(6px)}}.scrim{position:fixed;top:0;right:0;bottom:0;left:0;z-index:45;display:grid;place-items:center;padding:20px;background:#040609b3;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.dialog{width:min(520px,100%);max-height:86vh;overflow-y:auto;padding:20px 22px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);box-shadow:0 24px 60px #0009}.dialog h2{margin:0 0 6px;font-size:16px}.dialog p{margin:0 0 16px;color:var(--muted);font-size:13.5px}.dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.kv{display:flex;flex-direction:column}.kv-row{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid var(--line-soft);font-size:13.5px}.kv-row:last-child{border-bottom:0}.kv-row>span:first-child{color:var(--muted)}.kv-row>span:last-child{margin-left:auto;text-align:right;min-width:0}.tiles.plain{margin-bottom:12px;grid-template-columns:repeat(auto-fit,minmax(120px,1fr))}.tiles.plain .tile{padding:10px 12px;border:0;background:var(--surface-lift)}.tiles.plain .tile b{font-size:18px}.hint{margin:0;color:var(--quiet);font-size:12px}.grid.two{grid-template-columns:repeat(auto-fit,minmax(420px,1fr))}@media (max-width: 980px){.grid.two{grid-template-columns:1fr}}.list-row{display:flex;align-items:center;gap:12px;padding:12px 18px;border-bottom:1px solid var(--line-soft);color:var(--text);text-decoration:none}.list-row:last-child{border-bottom:0}.list-row:hover{background:var(--surface-lift)}.list-main{display:flex;align-items:center;gap:12px;flex:1 1 auto;min-width:0}.list-title{display:flex;align-items:center;gap:7px;font-size:14px;font-weight:600}.list-meta{display:block;margin-top:2px;color:var(--muted);font-size:12.5px}.list-row .list-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}.card.flush{padding:0}.avatar{display:grid;place-items:center;width:34px;height:34px;flex:0 0 34px;border-radius:50%;background:var(--note-wash);color:var(--note-ink);font-size:12.5px;font-weight:700;letter-spacing:.02em}.dot-state{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--quiet)}.dot-state[data-tone=ok]{background:var(--accent)}.dot-state[data-tone=warn]{background:var(--warn)}.dot-state[data-tone=bad]{background:var(--danger)}.checks.columns{display:grid;gap:0 24px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.crumb{color:var(--muted);font-size:12.5px;text-decoration:none;white-space:nowrap}.crumb:hover{color:var(--accent-ink)}.versions{display:flex;flex-wrap:wrap;gap:4px}.visit{padding:16px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-lift)}.visit+.visit{margin-top:10px}.visit header{display:flex;align-items:center;gap:12px;margin-bottom:14px}.visit header b{font-size:13.5px}.visit header span{display:block;color:var(--quiet);font-size:12px}.visit header .tag{margin-left:auto}.journey-answers{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;margin:0 0 15px;overflow:hidden;border:1px solid var(--line);border-radius:var(--radius-xs);background:var(--line)}.journey-answer{position:relative;min-width:0;padding:11px 12px 11px 38px;background:var(--surface)}.journey-answer .ico{position:absolute;top:13px;left:12px;width:16px;height:16px;color:var(--quiet)}.journey-answer[data-kind=entry] .ico,.journey-answer[data-kind=outcome] .ico{color:var(--accent)}.journey-answer[data-kind=selection] .ico{color:var(--note-ink)}.journey-answer span,.journey-answer b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.journey-answer span{margin-bottom:3px;color:var(--quiet);font-size:11px;font-weight:650;letter-spacing:.04em;text-transform:uppercase}.journey-answer b{color:var(--text);font-size:13px;font-weight:600}.journey-timeline{margin:0;padding:0 0 0 14px;list-style:none;border-left:1px solid var(--line)}.journey-timeline li{position:relative;display:flex;align-items:baseline;gap:10px;padding:7px 0 7px 5px;font-size:12.5px}.timeline-dot{position:absolute;left:-18px;top:12px;width:7px;height:7px;border-radius:50%;background:var(--accent)}.timeline-dot[data-action=select],.timeline-dot[data-action=open]{background:var(--note-ink)}.timeline-dot[data-action=close]{background:var(--quiet)}.journey-timeline li>div{min-width:0}.journey-timeline li b{display:inline;font-weight:600}.journey-timeline li div span{display:block;color:var(--muted)}.journey-timeline li time{margin-left:auto;padding-left:10px;color:var(--quiet);white-space:nowrap}.visits{max-height:60vh;overflow-y:auto}@media (max-width: 640px){.journey-answers{grid-template-columns:1fr}.journey-timeline li{align-items:flex-start;flex-wrap:wrap}.journey-timeline li time{width:100%;margin:2px 0 0;padding-left:0}}.summary{padding:13px 15px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-lift)}.summary-head{display:flex;align-items:baseline;gap:10px;margin-bottom:9px}.summary-head b{font-size:13px}.summary-head strong{margin-left:auto;font-size:22px;font-weight:650;letter-spacing:-.03em}.summary p{margin:9px 0 0;color:var(--muted);font-size:12.5px}.summary-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));margin-top:14px}.table-sub{display:block;color:var(--quiet);font-size:11.5px}.route-arrow{color:var(--quiet)}details summary{cursor:pointer;font-size:12.5px;padding:6px 0}.swatch{position:relative;width:34px;height:22px;flex:0 0 34px;margin-top:1px;border-radius:var(--radius-xs);background:var(--swatch-surface, var(--surface-hi));border:1px solid var(--swatch-hairline, var(--line));overflow:hidden}.swatch i{position:absolute;inset:auto 0 0 0;height:7px;background:var(--swatch-accent, var(--accent))}.group-label{margin:4px 0 2px;color:var(--quiet);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.group+.group{margin-top:14px;padding-top:14px;border-top:1px solid var(--line-soft)}.hero-pins{display:grid;gap:8px;margin-bottom:16px}.hero-pin{display:grid;grid-template-columns:30px minmax(0,1fr) auto auto;align-items:center;gap:10px;min-height:52px;padding:8px 10px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface-lift)}.hero-pin-order{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;background:var(--note-wash);color:var(--note-ink);font-size:12px;font-weight:750}.hero-pin>span:nth-child(2){min-width:0}.hero-pin b,.hero-pin small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hero-pin b{font-size:13.5px}.hero-pin small{margin-top:2px;color:var(--quiet);font-size:11.5px}.hero-schedule-list{display:grid;gap:10px}.hero-schedule{position:relative;display:grid;grid-template-columns:100px minmax(0,1fr) auto;align-items:center;gap:16px;padding:15px;overflow:hidden;border:1px solid var(--line);border-radius:11px;background:linear-gradient(135deg,var(--surface-lift),rgba(13,17,23,.74))}.hero-schedule:before{content:"";position:absolute;inset:0 auto 0 0;width:3px;background:var(--quiet);opacity:.4}.hero-schedule[data-enabled=true]:before{background:var(--info);opacity:1}.hero-schedule-time{align-self:stretch;display:flex;flex-direction:column;justify-content:center;padding-right:14px;border-right:1px solid var(--line-soft)}.hero-schedule-time b{color:var(--info-ink);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.hero-schedule-time span{margin-top:4px;color:var(--muted);font:12px/1.4 var(--mono)}.hero-schedule-main{min-width:0}.hero-schedule-title{display:flex;align-items:center;gap:8px}.hero-schedule-title h3{min-width:0;margin:0;overflow:hidden;color:var(--text);font-size:14px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.hero-schedule-main>p{margin:4px 0 8px;color:var(--muted);font-size:12.5px}.hero-schedule-actions{display:flex;align-items:center;gap:6px}.hero-schedule-dialog{width:min(760px,100%);padding:0;overflow-x:hidden}.hero-schedule-dialog-head{padding:24px 26px 20px;border-bottom:1px solid var(--line);background:radial-gradient(circle at 90% -40%,var(--info-wash),transparent 240px),var(--surface-lift)}.hero-schedule-kicker{color:var(--info-ink);font-size:10.5px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.hero-schedule-dialog-head h2{margin-top:4px;font-size:21px;letter-spacing:-.025em}.hero-schedule-dialog-head p{margin-bottom:0}.hero-schedule-dialog>.fields,.hero-schedule-dialog>.schedule-days,.hero-schedule-dialog>.schedule-options,.hero-schedule-dialog>.check{margin-right:26px;margin-left:26px}.schedule-frequency{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:20px 26px}.schedule-frequency button{display:block;height:auto;min-height:68px;padding:11px 13px;text-align:left;white-space:normal}.schedule-frequency button b,.schedule-frequency button span{display:block}.schedule-frequency button span{margin-top:4px;color:var(--quiet);font-size:11.5px;font-weight:500}.schedule-frequency button[aria-pressed=true]{border-color:var(--info);background:var(--info-wash);color:var(--info-ink)}.schedule-frequency button[aria-pressed=true] span{color:var(--muted)}.schedule-days{margin-top:18px;padding:16px;border:1px solid var(--line-soft);border-radius:10px;background:var(--surface-lift)}.schedule-days-head{display:flex;align-items:center;gap:12px;margin-bottom:12px}.schedule-days-head>b{font-size:12.5px}.schedule-days-head>div{display:flex;gap:4px;margin-left:auto}.schedule-days-head button{height:28px;padding:0 8px;border-color:transparent;background:none;color:var(--muted);font-size:11.5px}.schedule-day-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px}.schedule-day-grid button{width:100%;height:38px;padding:0;color:var(--muted)}.schedule-day-grid button[aria-pressed=true]{border-color:var(--accent);background:var(--accent-wash);color:var(--accent-ink)}.schedule-options{display:grid;grid-template-columns:minmax(0,2fr) minmax(140px,1fr);align-items:start;gap:22px;margin-top:20px;padding-top:18px;border-top:1px solid var(--line-soft)}.schedule-option-label{display:block;margin-bottom:3px;color:var(--muted);font-size:12px;font-weight:600}.schedule-placement-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px 14px}.hero-schedule-dialog>.check{margin-top:8px}.hero-schedule-dialog>.dialog-actions{margin-top:8px;padding:18px 26px;border-top:1px solid var(--line);background:var(--surface-lift)}@media (max-width: 820px){.hero-pin{grid-template-columns:30px minmax(0,1fr) auto}.hero-pin>button:last-child{grid-column:3}.hero-schedule{grid-template-columns:78px minmax(0,1fr);gap:12px}.hero-schedule-actions{grid-column:1 / -1;padding-top:10px;border-top:1px solid var(--line-soft)}.hero-schedule-actions button{flex:1 1 0}.hero-schedule-dialog-head{padding:22px 20px 18px}.hero-schedule-dialog>.fields,.hero-schedule-dialog>.schedule-days,.hero-schedule-dialog>.schedule-options,.hero-schedule-dialog>.check{margin-right:20px;margin-left:20px}.schedule-frequency{gap:6px;margin:16px 20px}.schedule-frequency button{min-width:0;min-height:62px;padding:9px}.schedule-frequency button span{display:none}.schedule-days-head{align-items:flex-start}.schedule-days-head>div{flex-wrap:wrap;justify-content:flex-end}.schedule-day-grid{gap:4px}.schedule-day-grid button{min-width:0;padding:0 3px;font-size:11px}.schedule-options,.schedule-placement-grid{grid-template-columns:1fr}.hero-schedule-dialog>.dialog-actions{padding:16px 20px max(16px,var(--safe-bottom))}}@media (pointer: coarse) and (min-width: 821px) and (max-width: 1400px){.page-head{margin-bottom:24px}.page-head h1{font-size:26px;line-height:1.2}.page-head p{max-width:72ch;font-size:14px}.card{padding:20px 22px;border-color:#636e7b6b;border-radius:12px;box-shadow:0 1px #ffffff06,0 14px 40px #00000024}.card+.card,.grid+.card,.card+.grid,.grid+.grid{margin-top:20px}.grid{gap:20px}.card-head{align-items:flex-start;margin-bottom:18px}.card-head h2{font-size:15.5px}.card-head p{margin-top:4px;font-size:13px}.card-foot{margin-top:18px;padding-top:16px}.tiles{gap:14px}.tile{min-height:112px;padding:16px 17px;border-radius:12px;background:linear-gradient(145deg,var(--surface-lift),var(--surface))}.tile b{font-size:27px}.filters{gap:12px;padding:16px;border-radius:12px}.filters .field{flex:1 1 calc(50% - 6px);max-width:none}.filters .field.grow{flex-basis:100%}.list-row{min-height:64px;padding:14px 20px}.list-item{min-height:60px;padding:13px 0}.loghead,.logbody{min-width:680px}.loghead,.logline[data-virtual=true]{grid-template-columns:128px 44px minmax(160px,1fr) minmax(220px,1.4fr);gap:10px}.logview{height:min(66dvh,720px)}}@media (max-width: 820px){.page-head-row{display:grid;grid-template-columns:minmax(0,1fr);gap:14px}.page-head-row .page-head-actions{width:100%;margin-left:0}.card{padding:17px 18px;border-radius:11px}.card-head{align-items:flex-start;flex-wrap:wrap}.card-head-actions{width:100%;margin-left:0}.card-foot>button,.card-foot>.btn{flex:1 1 auto}.tile{min-width:0;min-height:104px;padding:14px;background:linear-gradient(145deg,var(--surface-lift),var(--surface))}.tile b{font-size:23px}.tile span{overflow-wrap:anywhere}.filters .field,.filters .field.grow{flex:1 1 100%;max-width:none}.filters .filter-actions{width:100%;margin-left:0}.list-item,.list-row,.list-main{align-items:flex-start}.list-item,.list-row{flex-wrap:wrap}.list-item .list-actions,.list-row .list-actions{width:100%;justify-content:flex-start;padding-left:46px}.kv-row{align-items:flex-start}.dialog{width:100%;max-height:min(88dvh,720px);padding:22px;border-radius:16px}.scrim{align-items:end;padding:12px max(12px,var(--safe-right)) max(12px,var(--safe-bottom)) max(12px,var(--safe-left))}.dialog-actions>button{flex:1 1 0}.bell-panel{position:fixed;top:calc(var(--top) + 8px);right:max(12px,var(--safe-right));left:max(12px,var(--safe-left));width:auto;max-height:calc(100dvh - var(--top) - var(--safe-bottom) - 20px);border-radius:14px}.bell-list{max-height:calc(100dvh - var(--top) - var(--safe-bottom) - 120px)}.toasts{right:max(12px,var(--safe-right));bottom:max(12px,var(--safe-bottom));left:max(12px,var(--safe-left));max-width:none}}@media (pointer: coarse) and (max-width: 1400px){button,.btn{min-width:44px;height:44px;padding-right:15px;padding-left:15px}button[data-size=sm]{min-width:38px;height:38px;padding-right:11px;padding-left:11px}.topbar .rail-toggle,.topbar .bell-button,.topbar .topbar-status,.topbar .account-trigger{width:44px;height:44px;padding:0}.topbar .bell,.topbar .account-menu,.topbar .omni-input{height:44px}.rail a{min-height:46px;padding:10px 12px;border-radius:9px;font-size:14px}.rail-head{min-height:42px;padding:12px}input[type=text],input[type=password],input[type=search],input[type=number],input[type=date],input[type=time],input[type=datetime-local],input[type=url],select{height:44px;font-size:16px}select{background-position:calc(100% - 16px) 20px,calc(100% - 11px) 20px}textarea{min-height:104px;font-size:16px}.check{min-height:52px;padding-top:14px;padding-bottom:14px}.check .switch{width:40px;height:24px;flex-basis:40px}.check .switch:after{width:18px;height:18px}.check input:checked+.switch:after{transform:translate(16px)}.segments{max-width:100%;overflow-x:auto}.segments button{height:38px}.crumb,.crumbs a,.table-row-link{display:inline-flex;align-items:center;min-height:36px}}@media (pointer: fine) and (min-width: 821px) and (max-height: 800px){.page{padding-top:20px;padding-bottom:52px}.page-head{margin-bottom:16px}.card{padding-top:14px;padding-bottom:14px}.card-head{margin-bottom:11px}}@media (hover: none){tbody tr:hover td,.list-row:hover,.bell-item:hover,a.tile:hover{background:inherit}} diff --git a/admin-ui/dist/assets/index-cNUhbl7V.css b/admin-ui/dist/assets/index-cNUhbl7V.css new file mode 100644 index 0000000..b7b7b80 --- /dev/null +++ b/admin-ui/dist/assets/index-cNUhbl7V.css @@ -0,0 +1 @@ +@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2) format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2) format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2) format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-greek-wght-normal-CkhJZR-_.woff2) format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2) format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2) format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url(/admin/assets/inter-latin-wght-normal-Dx4kXJAl.woff2) format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}:root{color-scheme:dark;--oled: #000;--bg: #080b10;--rail-bg: var(--oled);--top-bg: rgba(1, 4, 9, .94);--surface: #0d1117;--surface-lift: #161b22;--surface-hi: #21262d;--line: #30363d;--line-soft: #21262d;--text: #f0f6fc;--muted: #b1bac4;--quiet: #7d8590;--accent: #2ea043;--accent-ink: #56d364;--accent-wash: rgba(46, 160, 67, .16);--idle: #4a8f60;--idle-ink: #86c79a;--idle-wash: rgba(74, 143, 96, .13);--danger: #e5534b;--danger-ink: #ff9b94;--danger-wash: rgba(229, 83, 75, .13);--warn: #e0ad4e;--warn-ink: #f2cb78;--warn-wash: rgba(239, 196, 107, .13);--info: #4f9cd8;--info-ink: #93cbef;--info-wash: rgba(79, 156, 216, .14);--note: #a07ce8;--note-ink: #bda1f5;--note-wash: rgba(160, 124, 232, .14);--data: #3fc2b6;--data-ink: #6fdcd0;--data-wash: rgba(63, 194, 182, .13);--rail: 236px;--safe-top: env(safe-area-inset-top, 0px);--safe-right: env(safe-area-inset-right, 0px);--safe-bottom: env(safe-area-inset-bottom, 0px);--safe-left: env(safe-area-inset-left, 0px);--top: calc(58px + var(--safe-top));--radius: 8px;--radius-sm: 6px;--radius-xs: 4px;--sans: "Inter Variable", Inter, "Segoe UI", sans-serif;--mono: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:radial-gradient(circle at 72% -20%,rgba(63,194,182,.045),transparent 38rem),var(--bg);color:var(--text);font:16.5px/1.55 var(--sans);font-weight:450;letter-spacing:0;-webkit-font-smoothing:antialiased;min-width:320px;min-height:100dvh;background:var(--bg);-webkit-tap-highlight-color:transparent}a{color:var(--accent-ink)}@media (prefers-reduced-motion: reduce){*,*:before,*:after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}}.skip{position:absolute;left:-9999px;top:8px;z-index:60;padding:8px 14px;border-radius:var(--radius-sm);background:var(--surface-lift);color:var(--text);text-decoration:none}.skip:focus{left:8px}:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:var(--radius-xs)}.topbar{position:fixed;inset:0 0 auto 0;z-index:30;height:var(--top);display:flex;align-items:center;gap:12px;padding:var(--safe-top) max(clamp(14px,2vw,22px),var(--safe-right)) 0 var(--safe-left);background:var(--top-bg);border-bottom:1px solid var(--line);box-shadow:0 1px #f0f6fc05,0 8px 24px #0003;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}.topbar-brand{display:flex;align-items:center;gap:11px;flex:0 0 var(--rail);height:100%;padding:0 22px;color:var(--text);text-decoration:none}.brand-mark{display:grid;place-items:center;width:30px;height:30px;flex:0 0 30px;border-radius:var(--radius-sm);background:var(--accent);color:#fff;font-size:16px;font-weight:800;letter-spacing:-.06em}.brand-word{font-size:17px;font-weight:650;letter-spacing:-.01em;white-space:nowrap}.topbar-spacer{flex:1 1 auto;min-width:0}.topbar-tools{display:flex;align-items:center;gap:10px;min-width:0}.topbar .omni{position:absolute;left:50%;transform:translate(-50%);width:min(820px,calc(100vw - var(--rail) - 380px))}.topbar-version{font:11.5px/1 var(--mono);color:var(--quiet);white-space:nowrap}.topbar-status{display:grid;place-items:center;width:38px;height:38px;flex:0 0 38px;padding:0;border:1px solid transparent;border-radius:var(--radius-sm);background:transparent;color:inherit;cursor:pointer;transition:background .16s ease,border-color .16s ease,color .16s ease}.topbar-status:hover:not(:disabled),.topbar-status:focus-visible{background:var(--surface-lift)}.topbar-status:disabled{cursor:default;opacity:1}.topbar-status .dot{width:9px;height:9px;border-radius:50%;background:var(--quiet);box-shadow:0 0 0 3px #7d85901f}.topbar-status[data-tone=ok] .dot{background:var(--accent-ink);box-shadow:0 0 0 3px var(--accent-wash),0 0 9px #56d36447}.topbar-status[data-tone=bad] .dot{background:var(--danger);box-shadow:0 0 0 3px var(--danger-wash)}.account-menu{position:relative;display:flex;align-items:center;height:38px;flex:0 0 auto}.account-trigger{max-width:190px;height:38px;padding:0 9px 0 5px;border-color:var(--line);background:var(--surface);color:var(--muted)}.account-trigger:hover:not(:disabled),.account-menu[data-open=true] .account-trigger{border-color:#56d36480;background:var(--surface-hi);color:var(--text)}.account-avatar{display:grid;place-items:center;width:27px;height:27px;flex:0 0 27px;border:1px solid rgba(86,211,100,.36);border-radius:50%;background:var(--accent-wash);color:var(--accent-ink);font-size:11px;font-weight:750}.account-name{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:12.5px}.account-caret{width:12px;height:12px;transition:transform .16s ease}.account-menu[data-open=true] .account-caret{transform:rotate(180deg)}.account-panel{position:absolute;top:calc(100% + 8px);right:0;width:min(280px,calc(100vw - 24px));padding:7px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:0 18px 44px #0000008c;z-index:40}.account-identity{display:flex;align-items:center;gap:11px;min-width:0;padding:9px 10px 12px;border-bottom:1px solid var(--line-soft)}.account-avatar-large{width:34px;height:34px;flex-basis:34px;font-size:13px}.account-identity span:last-child{min-width:0}.account-identity small,.account-identity b{display:block}.account-identity small{color:var(--quiet);font-size:11px}.account-identity b{overflow:hidden;text-overflow:ellipsis;font-size:13.5px}.account-panel>a,.account-panel form button{display:flex;align-items:center;gap:9px;justify-content:flex-start;width:100%;height:38px;padding:0 10px;border-radius:8px;color:var(--muted);font-size:13px;text-decoration:none}.account-panel>a:hover{background:var(--surface-hi);color:var(--text)}.account-panel>a{margin-top:6px}.account-panel form{margin-top:2px}.account-panel form button{justify-content:flex-start;width:100%;height:38px;border-color:transparent;background:transparent;color:var(--muted)}.account-panel form button:hover:not(:disabled){background:var(--surface-hi);color:var(--text)}@media (max-width: 900px){.topbar-version{display:none}}.omni{position:relative;width:100%}.omni-input{display:flex;align-items:center;gap:8px;width:100%;height:38px;padding:0 10px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface)}.omni[data-open=true] .omni-input{border-color:var(--accent)}.omni-input input[type=search]{flex:1 1 auto;min-width:0;height:100%;padding:0;border:0;border-radius:0;background:none;color:var(--text);font:inherit;font-size:14px;outline:none}.omni-input input::placeholder{color:var(--quiet)}.omni-input .ico{color:var(--quiet);flex:0 0 15px}.omni-key{flex:0 0 auto;padding:2px 6px;border:1px solid var(--line);border-radius:4px;font:10.5px/1.3 var(--mono);color:var(--quiet)}.omni-panel{position:absolute;top:calc(100% + 6px);right:auto;left:0;width:100%;max-height:min(50vh,420px);overflow-y:auto;padding:6px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);box-shadow:0 18px 44px #0000008c;z-index:40}.omni-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:var(--radius-sm);color:var(--text);text-decoration:none}.omni-item .ico{color:var(--quiet);flex:0 0 16px}.omni-item.on,.omni-item:hover{background:var(--surface-hi)}.omni-item b{font-size:13.5px;font-weight:600}.omni-item small{display:block;color:var(--quiet);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.omni-item span{flex:1 1 auto;min-width:0}.omni-group{flex:0 0 auto;color:var(--quiet);font-size:10.5px;letter-spacing:.08em;text-transform:uppercase}.bell{position:relative;display:flex;align-items:center;height:38px}.bell-button{position:relative;display:grid;place-items:center;width:38px;height:38px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--muted);cursor:pointer}.bell-button:hover{background:var(--surface-hi);color:var(--text)}.bell[data-open=true] .bell-button{border-color:var(--accent);color:var(--text)}.bell-badge{position:absolute;top:-6px;right:-6px;min-width:17px;height:17px;padding:0 4px;border-radius:9px;background:var(--danger);color:#fff;font:700 10.5px/17px var(--sans);text-align:center}.bell-panel{position:absolute;top:calc(100% + 8px);right:0;width:min(420px,92vw);border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);box-shadow:0 18px 44px #0000008c;z-index:40;overflow:hidden}.bell-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;border-bottom:1px solid var(--line)}.bell-head b{font-size:13px}.bell-list{max-height:min(58vh,460px);overflow-y:auto}.bell-item{display:flex;gap:11px;padding:11px 14px;border-bottom:1px solid var(--line-soft);color:var(--text);text-decoration:none}.bell-item:last-child{border-bottom:0}.bell-item:hover{background:var(--surface-lift)}.bell-item[data-unread=true]{box-shadow:inset 2px 0 0 var(--accent)}.bell-item .glyph{flex:0 0 auto}.bell-body{flex:1 1 auto;min-width:0}.bell-body b{display:block;font-size:13px;font-weight:600}.bell-body p{margin:2px 0 0;color:var(--muted);font-size:12.5px;overflow-wrap:anywhere}.bell-body time{display:block;margin-top:3px;color:var(--quiet);font-size:11px}.bell-foot{padding:9px 14px;border-top:1px solid var(--line);text-align:center}.bell-foot a{font-size:12.5px;text-decoration:none}.rail{position:fixed;top:var(--top);bottom:0;left:0;width:var(--rail);padding:12px 12px max(20px,var(--safe-bottom));overflow-y:auto;background:var(--rail-bg);border-right:1px solid var(--line);z-index:20}.rail-group+.rail-group{margin-top:4px}.rail-group:first-child{margin-bottom:9px;padding-bottom:9px;border-bottom:1px solid var(--line-soft)}.rail-head{display:flex;align-items:center;gap:6px;width:100%;padding:9px 10px 5px;border:0;background:none;color:var(--quiet);font:600 10.5px/1 var(--sans);letter-spacing:.1em;text-transform:uppercase;cursor:pointer}.rail-head .caret{margin-left:auto;transition:transform .12s ease}.rail-head[aria-expanded=false] .caret{transform:rotate(-90deg)}.rail-head-static{cursor:default;color:var(--muted)}.rail a{display:flex;align-items:center;gap:10px;padding:7px 10px;border-radius:var(--radius-sm);color:var(--muted);font-size:13.5px;text-decoration:none}.rail a:hover{background:var(--surface);color:var(--text)}.rail a[aria-current=page]{background:var(--accent-wash);color:var(--accent-ink);font-weight:600}.rail a .ico{flex:0 0 17px;opacity:.85}.rail a[aria-current=page] .ico{opacity:1}.rail-badge{margin-left:auto;min-width:18px;padding:0 5px;border-radius:9px;background:var(--danger);color:#fff;font:700 10.5px/17px var(--sans);text-align:center}.page{width:min(calc(100vw - var(--rail)),1920px);min-width:0;margin:var(--top) 0 0 calc(var(--rail) + max(0px,(100vw - var(--rail) - 1920px) / 2));padding:clamp(24px,2.4vw,44px) clamp(22px,3vw,56px) 80px}.page-head{margin-bottom:20px}.page-head h1{margin:0;font-size:22px;font-weight:650;letter-spacing:-.02em}.page-head p{margin:5px 0 0;max-width:68ch;color:var(--muted);font-size:13.5px}.page-head-row{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap}.page-head-title{display:flex;align-items:flex-start;gap:12px;min-width:0}.page-head-icon{display:grid;place-items:center;flex:0 0 40px;width:40px;height:40px;border:1px solid var(--line);border-radius:10px;background:var(--accent-wash);color:var(--accent-ink)}.page-head-icon .ico{width:20px;height:20px}.page-head-text{min-width:0}.page-head-row .page-head-actions{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}.crumbs{display:flex;align-items:center;gap:6px;margin-bottom:8px;color:var(--quiet);font-size:12px}.crumbs a{color:var(--muted);text-decoration:none}.crumbs a:hover{color:var(--text)}@media (max-width: 1400px){:root{--rail: 0px}.rail{transform:translate(-100%);transition:transform .2s cubic-bezier(.22,1,.36,1);width:min(320px,calc(100vw - 72px));padding-right:14px;padding-left:max(14px,var(--safe-left));background:#080c12fa;box-shadow:none}.rail[data-open=true]{transform:none;box-shadow:24px 0 80px #00000094}.topbar-brand{flex:0 0 auto;padding:0 4px 0 12px}.page{margin-left:0}}.rail-scrim{display:none}@media (max-width: 1400px){.rail-scrim{position:fixed;inset:var(--top) 0 0 0;z-index:19;display:block;width:auto;height:auto;padding:0;border:0;border-radius:0;background:#01040994;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);animation:rail-scrim-in .16s ease-out}.rail-scrim:hover:not(:disabled){border:0;background:#01040994}}@keyframes rail-scrim-in{0%{opacity:0}}.rail-toggle{display:none;width:34px;height:34px;margin-left:8px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--muted);cursor:pointer}@media (max-width: 1400px){.rail-toggle{display:grid;place-items:center}}.banner{display:flex;align-items:center;gap:10px;margin-bottom:16px;padding:11px 14px;border:1px solid var(--danger);border-radius:var(--radius-sm);background:var(--danger-wash);color:var(--danger-ink);font-size:13.5px}.banner button{margin-left:auto;border:0;background:none;color:inherit;cursor:pointer;font:inherit}.card{padding:16px 18px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.card+.card,.grid+.card,.card+.grid,.grid+.grid{margin-top:16px}.card-head{display:flex;align-items:center;gap:11px;margin-bottom:14px}.card-head h2{margin:0;font-size:14.5px;font-weight:620;letter-spacing:-.01em}.card-head p{margin:2px 0 0;color:var(--muted);font-size:12.5px}.card-head-text{min-width:0}.card-head-actions{margin-left:auto;display:flex;align-items:center;gap:8px;flex-wrap:wrap}.card-foot{margin-top:14px;padding-top:13px;border-top:1px solid var(--line-soft);display:flex;align-items:center;gap:8px;flex-wrap:wrap}.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.grid[data-cols="2"]{grid-template-columns:repeat(auto-fit,minmax(400px,1fr))}.grid[data-cols=wide]{grid-template-columns:minmax(0,2fr) minmax(300px,1fr)}@media (max-width: 1100px){.grid[data-cols=wide]{grid-template-columns:1fr}}.tiles{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(158px,1fr));margin-bottom:16px}.tile{padding:13px 15px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.tile .glyph{margin:0 0 9px}.tile b{display:block;font-size:24px;font-weight:650;letter-spacing:-.03em;line-height:1.15}.tile b.small{font-size:14.5px;font-weight:600;letter-spacing:-.01em;line-height:1.4}.tile span{display:block;margin-top:3px;color:var(--muted);font-size:12px}a.tile{color:inherit;text-decoration:none}a.tile:hover{border-color:var(--accent);background:var(--surface-lift)}.ico{display:block;width:1em;height:1em;font-size:17px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:0 0 auto}.glyph{display:grid;place-items:center;width:30px;height:30px;flex:0 0 30px;border-radius:var(--radius-sm);background:var(--surface-hi);color:var(--muted)}.tile .glyph .ico{width:18px;height:18px;margin:auto}.loading-page{display:grid;gap:20px}.loading-heading{display:grid;gap:10px;max-width:48rem}.loading-heading .skeleton:first-child{height:34px;width:28%}.loading-heading .skeleton:last-child{height:18px;width:72%}.loading-tiles{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:16px}.loading-tiles .skeleton{height:132px}.loading-card{display:grid;gap:12px;min-height:180px;padding:22px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.loading-card .skeleton:first-child{height:20px;width:32%}.loading-card .skeleton:not(:first-child){height:14px}@media (max-width: 820px){.loading-tiles{grid-template-columns:repeat(2,minmax(0,1fr))}}.glyph .ico{display:block}.glyph[data-tone=ok]{background:var(--accent-wash);color:var(--accent-ink)}.glyph[data-tone=idle]{background:var(--idle-wash);color:var(--idle-ink)}.glyph[data-tone=warn]{background:var(--warn-wash);color:var(--warn-ink)}.glyph[data-tone=bad]{background:var(--danger-wash);color:var(--danger-ink)}.glyph[data-tone=info]{background:var(--info-wash);color:var(--info-ink)}.glyph[data-tone=note]{background:var(--note-wash);color:var(--note-ink)}.glyph[data-tone=data]{background:var(--data-wash);color:var(--data-ink)}.tag{display:inline-flex;align-items:center;gap:5px;padding:2px 8px;border:1px solid var(--line);border-radius:999px;background:var(--surface-lift);color:var(--muted);font-size:11.5px;font-weight:600;white-space:nowrap}.tag[data-tone]:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}.tag[data-tone=ok]{border-color:#52b54b59;background:var(--accent-wash);color:var(--accent-ink)}.tag[data-tone=idle]{border-color:#4a8f6059;background:var(--idle-wash);color:var(--idle-ink)}.tag[data-tone=warn]{border-color:#efc46b59;background:var(--warn-wash);color:var(--warn-ink)}.tag[data-tone=bad]{border-color:#e5534b59;background:var(--danger-wash);color:var(--danger-ink)}.tag[data-tone=info]{border-color:#4f9cd859;background:var(--info-wash);color:var(--info-ink)}.tag[data-tone=note]{border-color:#a07ce859;background:var(--note-wash);color:var(--note-ink)}.tag[data-tone=data]{border-color:#3fc2b659;background:var(--data-wash);color:var(--data-ink)}.chip{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border:1px solid var(--line);border-radius:var(--radius-xs);background:var(--surface-lift);color:var(--muted);font:11.5px/1.5 var(--mono);white-space:nowrap}.chip[data-tone=ok]{color:var(--accent-ink)}.chip[data-tone=idle]{color:var(--idle-ink)}.chip[data-tone=warn]{color:var(--warn-ink)}.chip[data-tone=bad]{color:var(--danger-ink)}.chip[data-tone=info]{color:var(--info-ink)}.chip[data-tone=note]{color:var(--note-ink)}.chip[data-tone=data]{color:var(--data-ink)}.chips{display:flex;flex-wrap:wrap;gap:6px}.table-wrap{overflow-x:auto;overscroll-behavior-inline:contain;-webkit-overflow-scrolling:touch;margin:0 -18px -16px;padding:0 18px 16px}table{width:100%;min-width:max-content;border-collapse:collapse;font-size:13px}@media (max-width: 1400px){.topbar{display:flex;gap:10px;height:var(--top);padding:var(--safe-top) max(16px,var(--safe-right)) 0 max(16px,var(--safe-left))}.topbar-brand{padding:0}.rail-toggle{margin-left:0}.topbar-spacer{display:none}.topbar .omni{position:relative;left:auto;transform:none;flex:1 1 auto;width:auto;max-width:640px;min-width:0}.omni-key,.topbar-version{display:none}.topbar-tools{display:contents}.bell-button,.rail-toggle{width:40px;height:40px}.topbar-status{width:40px;flex-basis:40px;min-width:40px;height:40px;border:1px solid var(--line);background:var(--surface)}.omni-input,.bell,.bell-button,.account-menu,.account-trigger{height:40px}.page{width:100%;padding:clamp(24px,3vw,34px) max(24px,calc(var(--safe-right) + 10px)) max(64px,calc(var(--safe-bottom) + 36px)) max(24px,calc(var(--safe-left) + 10px))}.table-wrap{margin:0 -18px -16px;padding:0 18px 18px;scrollbar-gutter:stable}table{font-size:13px}th,td{padding-right:16px}}@media (max-width: 1100px){.account-trigger{width:40px;padding:0}.account-name,.account-caret{display:none}}@media (max-width: 820px){.brand-word{display:none}.topbar-status{display:grid;place-items:center;width:40px;padding:0}.page{padding:22px max(16px,var(--safe-right)) max(56px,calc(var(--safe-bottom) + 28px)) max(16px,var(--safe-left))}.grid,.grid[data-cols="2"],.grid[data-cols=wide],.grid.two{grid-template-columns:minmax(0,1fr)}.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}.table-wrap{margin-right:-16px;margin-left:-16px;padding-right:16px;padding-left:16px}}@media (max-width: 680px){:root{--top: calc(60px + var(--safe-top))}.topbar{align-items:center;flex-wrap:nowrap;gap:clamp(4px,1.5vw,8px);padding:calc(var(--safe-top) + 8px) max(8px,var(--safe-right)) 8px max(8px,var(--safe-left))}.rail-toggle{order:1}.topbar-brand{order:2;height:40px;flex:0 0 auto}.topbar .omni{order:3;flex:1 1 96px;width:0;min-width:0;max-width:none}.topbar-status{order:4;margin-left:0}.bell{order:5}.account-menu{order:6}.brand-mark{width:30px;height:30px;flex-basis:30px}.account-panel,.bell-panel{position:fixed;top:calc(var(--top) + 8px)}.account-panel{right:max(12px,var(--safe-right))}}@media (max-width: 370px){.tiles{grid-template-columns:minmax(0,1fr)}}th{padding:0 12px 8px 0;border-bottom:1px solid var(--line);color:var(--quiet);font-weight:600;font-size:11px;letter-spacing:.07em;text-transform:uppercase;text-align:left;white-space:nowrap}td{padding:9px 12px 9px 0;border-bottom:1px solid var(--line-soft);vertical-align:middle}tr:last-child td{border-bottom:0}tbody tr:hover td{background:var(--surface-lift)}th:last-child,td:last-child{padding-right:0}td.num,th.num{text-align:right;font-variant-numeric:tabular-nums}td.mono{font:12px/1.5 var(--mono);color:var(--muted)}td.nowrap,th.nowrap{white-space:nowrap}td.muted{color:var(--muted)}th button{display:inline-flex;align-items:center;gap:4px;border:0;padding:0;background:none;color:inherit;font:inherit;letter-spacing:inherit;text-transform:inherit;cursor:pointer}th button:hover{color:var(--text)}th[aria-sort] button{color:var(--accent-ink)}.table-row-link{color:var(--text);text-decoration:none;font-weight:600}.table-row-link:hover{color:var(--accent-ink)}.list{display:flex;flex-direction:column;gap:1px}.list-item{display:flex;align-items:center;gap:12px;padding:11px 0;border-bottom:1px solid var(--line-soft)}.list-item:last-child{border-bottom:0}.list-item .list-body{flex:1 1 auto;min-width:0}.list-item b{display:block;font-size:13.5px;font-weight:600}.list-item p{margin:2px 0 0;color:var(--muted);font-size:12.5px}.list-item .list-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap}button,.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;height:32px;padding:0 13px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-lift);color:var(--text);font:600 13px/1 var(--sans);text-decoration:none;cursor:pointer;white-space:nowrap}button:hover:not(:disabled),.btn:hover{background:var(--surface-hi);border-color:#2c3542}button:disabled{opacity:.45;cursor:not-allowed}button[data-variant=primary]{border-color:var(--accent);background:var(--accent);color:#06240a}button[data-variant=primary]:hover:not(:disabled){background:var(--accent-ink);border-color:var(--accent-ink)}button[data-variant=danger]{border-color:#e5534b66;background:var(--danger-wash);color:var(--danger-ink)}button[data-variant=danger]:hover:not(:disabled){background:#e5534b38}button[data-variant=quiet]{border-color:transparent;background:none;color:var(--muted)}button[data-variant=quiet]:hover:not(:disabled){background:var(--surface-lift);color:var(--text)}button[data-size=sm]{height:26px;padding:0 9px;font-size:12px}.field{display:block;min-width:0}.field>span{display:block;margin-bottom:5px;color:var(--muted);font-size:12px;font-weight:600}.field>small{display:block;margin-top:5px;color:var(--quiet);font-size:11.5px}input[type=text],input[type=password],input[type=search],input[type=number],input[type=date],input[type=time],input[type=datetime-local],input[type=url],select,textarea{width:100%;height:32px;padding:0 10px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font:inherit;font-size:13px}textarea{height:auto;min-height:74px;padding:8px 10px;resize:vertical}input:focus,select:focus,textarea:focus{border-color:var(--accent);outline:none}input::placeholder,textarea::placeholder{color:var(--quiet)}select{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding-right:26px;background-image:linear-gradient(45deg,transparent 50%,var(--quiet) 50%),linear-gradient(135deg,var(--quiet) 50%,transparent 50%);background-position:calc(100% - 14px) 14px,calc(100% - 9px) 14px;background-size:5px 5px;background-repeat:no-repeat}.fields{display:grid;gap:13px;grid-template-columns:repeat(auto-fit,minmax(190px,1fr))}.field-row{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap}.check{display:flex;align-items:flex-start;gap:11px;padding:10px 0;cursor:pointer}.check input{position:absolute;opacity:0;pointer-events:none}.check .switch{position:relative;width:34px;height:20px;flex:0 0 34px;margin-top:1px;border-radius:999px;background:var(--surface-hi);border:1px solid var(--line);transition:background .12s ease,border-color .12s ease}.check .switch:after{content:"";position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;background:var(--muted);transition:transform .12s ease,background .12s ease}.check input:checked+.switch{background:var(--accent-wash);border-color:var(--accent)}.check input:checked+.switch:after{transform:translate(14px);background:var(--accent)}.check input:focus-visible+.switch{outline:2px solid var(--accent);outline-offset:2px}.check input:disabled+.switch{opacity:.45}.check-body b{display:block;font-size:13.5px;font-weight:600}.check-body p{margin:2px 0 0;color:var(--muted);font-size:12.5px}.segments{display:inline-flex;padding:2px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface)}.segments button{height:26px;border:0;background:none;color:var(--muted);font-size:12.5px;font-weight:600}.segments button[aria-pressed=true]{background:var(--surface-hi);color:var(--text)}.filters{display:flex;align-items:flex-end;gap:10px;flex-wrap:wrap;margin-bottom:14px;padding:13px 15px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.filters .field{flex:1 1 160px;max-width:260px}.filters .field.grow{flex:2 1 240px;max-width:none}.filters .filter-actions{margin-left:auto;display:flex;align-items:center;gap:8px}.filter-summary{color:var(--quiet);font-size:12px;white-space:nowrap}.bars{display:flex;align-items:flex-end;gap:3px;height:92px;padding-top:6px}.bar{position:relative;flex:1 1 0;min-width:3px;border-radius:2px 2px 0 0;background:var(--accent-wash);border-top:2px solid var(--accent);min-height:2px}.bar[data-tone=bad]{background:var(--danger-wash);border-top-color:var(--danger)}.bar[data-empty=true]{background:var(--line-soft);border-top-color:var(--line)}.bars-axis{display:flex;justify-content:space-between;margin-top:6px;color:var(--quiet);font-size:11px}.meter{height:6px;border-radius:3px;background:var(--surface-hi);overflow:hidden}.meter>div{height:100%;background:var(--accent)}.meter[data-tone=warn]>div{background:var(--warn)}.meter[data-tone=bad]>div{background:var(--danger)}.logbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;margin-bottom:10px}.logbar-filters{display:flex;flex:1 1 520px;flex-wrap:wrap;align-items:center;gap:6px;min-width:0}.logbar-filters select{width:auto;min-width:0;height:30px;padding:0 26px 0 9px;font-size:12px}.logbar-actions{display:flex;flex:0 0 auto;gap:6px;margin-left:auto}.logsearch{position:relative;display:flex;flex:1 1 260px;align-items:center;min-width:200px}.logsearch svg{position:absolute;left:9px;width:14px;height:14px;color:var(--quiet);pointer-events:none}.logsearch input{width:100%;height:30px;padding-left:28px;font-size:12px}.logchips{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}.logchip{display:inline-flex;align-items:center;gap:5px;height:24px;padding:0 8px;border:1px solid var(--line);border-radius:999px;background:var(--surface-lift);color:var(--muted);font:500 11px/1 var(--sans)}.logchip svg{width:11px;height:11px;opacity:.7}.logchip:hover:not(:disabled){border-color:var(--danger);background:var(--danger-wash);color:var(--danger-ink)}.logchip-clear{border-style:dashed;color:var(--quiet)}.logshell{position:relative}.logview{height:62vh;min-height:320px;overflow:auto;border:1px solid var(--line);border-radius:var(--radius-sm);background:#080a0e;font:12px/1.5 var(--sans);contain:layout paint style}.loghead,.logrow{display:grid;grid-template-columns:92px 52px minmax(150px,190px) minmax(240px,1fr) minmax(96px,150px) 68px;gap:12px;padding:0 12px}.loghead{position:sticky;top:0;z-index:2;align-items:center;height:31px;border-bottom:1px solid var(--line);background:#10131a;color:var(--quiet);font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;min-width:900px}.loghead>:last-child{text-align:right}.logbody{position:relative;min-width:900px}.logday{position:absolute;top:0;right:0;left:0;display:flex;align-items:center;height:26px;padding:0 12px;background:linear-gradient(to bottom,rgba(255,255,255,.03),transparent);color:var(--quiet);font:700 10px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase}.logday span{padding-right:10px;background:#080a0e}.logday:after{content:"";flex:1;height:1px;background:var(--line-soft)}.logrow{position:absolute;top:0;right:0;left:0;align-items:center;border-bottom:1px solid rgba(255,255,255,.03);contain:strict}.logrow:hover{background:#ffffff09}.logrow[data-selected]{background:var(--accent-wash)}.logrow[data-level=ERROR]{box-shadow:inset 2px 0 0 var(--danger);background:#e5534b0d}.logrow[data-level=WARN]{box-shadow:inset 2px 0 0 var(--warn)}.logrow-time{color:var(--quiet);font:11px/1 var(--mono);font-variant-numeric:tabular-nums}.logfacet{height:auto;min-width:0;padding:0;overflow:hidden;border:0;background:none;color:inherit;font:inherit;text-align:left;text-overflow:ellipsis;white-space:nowrap}.logfacet:hover:not(:disabled){border:0;background:none;text-decoration:underline;text-underline-offset:3px}.logfacet:focus-visible{outline:1px solid var(--accent);outline-offset:2px}.logrow-level{color:var(--quiet);font:700 10px/1 var(--sans);letter-spacing:.06em}.logrow-level[data-level=ERROR]{color:var(--danger-ink)}.logrow-level[data-level=WARN]{color:var(--warn-ink)}.logrow-level[data-level=INFO]{color:var(--muted)}.logrow-level[data-level=DEBUG],.logrow-level[data-level=TRACE]{color:var(--quiet)}.logrow-place{display:flex;align-items:baseline;gap:5px;min-width:0}.logrow-service{flex:0 0 auto;max-width:96px;overflow:hidden;color:var(--quiet);font:700 10px/1.4 var(--sans);letter-spacing:.07em;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}.logrow-service[data-tone=info]{color:var(--info-ink)}.logrow-service[data-tone=note]{color:var(--note-ink)}.logrow-service[data-tone=data]{color:var(--data-ink)}.logrow-service[data-tone=idle]{color:var(--idle-ink)}.logrow-service[data-tone=quiet]{color:var(--muted)}.logrow-sep{flex:0 0 auto;color:var(--line)}.logrow-component{flex:1 1 auto;color:var(--quiet);font-size:11.5px}.logrow-summary{display:flex;flex-direction:column;gap:2px;justify-content:center;width:100%;height:100%;min-width:0;padding:0;overflow:hidden;border:0;background:none;font:inherit;text-align:left}.logrow-summary:hover:not(:disabled){border:0;background:none}.logrow-summary:focus-visible{outline:1px solid var(--accent);outline-offset:-1px}.logrow-line{display:flex;align-items:baseline;gap:8px;min-width:0}.logrow-action{flex:0 0 auto;color:var(--muted);font:600 10.5px/1.4 var(--mono);letter-spacing:.04em}.logrow-action[data-method=POST],.logrow-action[data-method=PUT],.logrow-action[data-method=PATCH]{color:var(--info-ink)}.logrow-action[data-method=DELETE]{color:var(--danger-ink)}.logrow-text{flex:1 1 auto;min-width:0;overflow:hidden;color:var(--text);text-overflow:ellipsis;white-space:nowrap}.logrow-error{overflow:hidden;color:var(--danger-ink);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.logrow-context{overflow:hidden;color:var(--quiet);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.logrow-result{min-width:0}.logrow-verdict{display:inline-block;max-width:100%;color:var(--quiet);font:500 11px/1.5 var(--mono)}.logrow-verdict[data-tone=ok]{color:var(--muted)}.logrow-verdict[data-tone=info]{color:var(--info-ink)}.logrow-verdict[data-tone=data]{color:var(--data-ink)}.logrow-verdict[data-tone=warn],.logrow-verdict[data-tone=bad]{padding:1px 6px;border-radius:var(--radius-xs);font-weight:600}.logrow-verdict[data-tone=warn]{background:var(--warn-wash);color:var(--warn-ink)}.logrow-verdict[data-tone=bad]{background:var(--danger-wash);color:var(--danger-ink)}.logrow-duration{color:var(--quiet);font:11px/1 var(--mono);font-variant-numeric:tabular-nums;text-align:right}.logrow-duration[data-tone=warn]{color:var(--warn-ink)}.logrow-duration[data-tone=bad]{color:var(--danger-ink);font-weight:600}.logtail{position:absolute;right:18px;bottom:14px;z-index:3;display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 12px;border:1px solid var(--accent);border-radius:999px;background:var(--surface-lift);color:var(--accent-ink);font:600 11px/1 var(--sans);box-shadow:0 6px 16px #00000080}.logtail svg{width:12px;height:12px;transform:rotate(90deg)}.logtail:hover:not(:disabled){border-color:var(--accent);background:var(--accent-wash);color:var(--accent-ink)}.logdrawer{margin-top:12px;padding:14px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface)}.logdrawer-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding-bottom:12px;border-bottom:1px solid var(--line)}.logdrawer-head>div:first-child{min-width:0}.logdrawer-head b{display:block;margin-top:4px;font-size:14px;overflow-wrap:anywhere}.logdrawer-place{display:flex;align-items:baseline;gap:5px;margin:0;color:var(--quiet);font-size:11px}.logdrawer-error{margin:6px 0 0;color:var(--danger-ink);font:12px/1.5 var(--mono);overflow-wrap:anywhere}.logdrawer-actions{display:flex;flex:0 0 auto;gap:6px}.logdrawer-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:16px 24px;margin-top:12px}.logdrawer-section h4{margin:0 0 6px;color:var(--quiet);font:700 10px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase}.logdrawer-section dl{display:grid;grid-template-columns:minmax(88px,max-content) minmax(0,1fr);gap:4px 12px;margin:0;font:12px/1.5 var(--mono)}.logdrawer-section dt{color:var(--muted)}.logdrawer-section dd{min-width:0;margin:0;overflow-wrap:anywhere}.logdrawer-raw{margin-top:14px;padding-top:12px;border-top:1px solid var(--line)}.logdrawer-raw summary{cursor:pointer;color:var(--quiet);font:700 10px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase}.logdrawer-raw pre{max-height:320px;margin:10px 0 0;overflow:auto;padding:10px;border:1px solid var(--line);border-radius:var(--radius-xs);background:#080a0e;font:11px/1.6 var(--mono)}@media (max-width: 1180px){.loghead,.logrow{grid-template-columns:84px 46px minmax(130px,170px) minmax(200px,1fr) minmax(88px,130px);gap:10px}.loghead>:last-child,.logrow-duration{display:none}.loghead,.logbody{min-width:640px}}@media (max-width: 900px){.loghead,.logrow{grid-template-columns:46px minmax(110px,140px) minmax(180px,1fr) minmax(72px,110px);gap:8px}.loghead>:first-child,.logrow-time{display:none}.loghead,.logbody{min-width:520px}}.logdetails{min-width:0}.logdetails summary{cursor:pointer;list-style:none;overflow-wrap:anywhere}.logdetails summary::-webkit-details-marker{display:none}.logdetails summary:before{content:"›";display:inline-block;margin-right:6px;color:var(--accent)}.logdetails[open] summary:before{transform:rotate(90deg)}.logdetails dl{display:grid;grid-template-columns:minmax(130px,max-content) minmax(0,1fr);gap:4px 12px;margin:7px 0 2px;padding:8px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#ffffff06}.logdetails dt{color:var(--muted)}.logdetails dd{min-width:0;margin:0;color:var(--text);overflow-wrap:anywhere}pre.code{margin:0;padding:12px 14px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#080a0e;color:var(--muted);font:12px/1.6 var(--mono);overflow-x:auto}.empty{margin:0;padding:22px 0;color:var(--quiet);font-size:13px;text-align:center}.muted{color:var(--muted)}.quiet{color:var(--quiet)}.mono{font-family:var(--mono);font-size:12px}.note{margin:0;padding:10px 13px;border:1px solid var(--line);border-left:2px solid var(--info);border-radius:var(--radius-xs);background:var(--surface-lift);color:var(--muted);font-size:12.5px}.note[data-tone=warn]{border-left-color:var(--warn)}.note[data-tone=bad]{border-left-color:var(--danger)}.note[data-tone=ok]{border-left-color:var(--accent)}.stack{display:flex;flex-direction:column;gap:12px}.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.row.tight{gap:6px}.spacer{flex:1 1 auto}.spinner{width:14px;height:14px;border:2px solid var(--line);border-top-color:var(--accent);border-radius:50%;animation:spin .7s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.skeleton{border-radius:var(--radius-sm);background:linear-gradient(90deg,var(--surface) 25%,var(--surface-lift) 50%,var(--surface) 75%);background-size:300% 100%;animation:shimmer 1.3s ease-in-out infinite}@keyframes shimmer{to{background-position:-300% 0}}.toasts{position:fixed;right:18px;bottom:18px;z-index:50;display:flex;flex-direction:column;gap:8px;max-width:min(420px,90vw)}.toast{display:flex;align-items:flex-start;gap:10px;padding:11px 14px;border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--radius-sm);background:var(--surface-lift);box-shadow:0 14px 34px #00000080;font-size:13px;animation:toast-in .14s ease}.toast[data-tone=bad]{border-left-color:var(--danger)}.toast[data-tone=warn]{border-left-color:var(--warn)}.toast button{margin-left:auto;height:auto;padding:0;border:0;background:none;color:var(--quiet)}@keyframes toast-in{0%{opacity:0;transform:translateY(6px)}}.scrim{position:fixed;top:0;right:0;bottom:0;left:0;z-index:45;display:grid;place-items:center;padding:20px;background:#040609b3;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}.dialog{width:min(520px,100%);max-height:86vh;overflow-y:auto;padding:20px 22px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);box-shadow:0 24px 60px #0009}.dialog h2{margin:0 0 6px;font-size:16px}.dialog p{margin:0 0 16px;color:var(--muted);font-size:13.5px}.dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.kv{display:flex;flex-direction:column}.kv-row{display:flex;align-items:center;gap:12px;padding:9px 0;border-bottom:1px solid var(--line-soft);font-size:13.5px}.kv-row:last-child{border-bottom:0}.kv-row>span:first-child{color:var(--muted)}.kv-row>span:last-child{margin-left:auto;text-align:right;min-width:0}.tiles.plain{margin-bottom:12px;grid-template-columns:repeat(auto-fit,minmax(120px,1fr))}.tiles.plain .tile{padding:10px 12px;border:0;background:var(--surface-lift)}.tiles.plain .tile b{font-size:18px}.hint{margin:0;color:var(--quiet);font-size:12px}.grid.two{grid-template-columns:repeat(auto-fit,minmax(420px,1fr))}@media (max-width: 980px){.grid.two{grid-template-columns:1fr}}.list-row{display:flex;align-items:center;gap:12px;padding:12px 18px;border-bottom:1px solid var(--line-soft);color:var(--text);text-decoration:none}.list-row:last-child{border-bottom:0}.list-row:hover{background:var(--surface-lift)}.list-main{display:flex;align-items:center;gap:12px;flex:1 1 auto;min-width:0}.list-title{display:flex;align-items:center;gap:7px;font-size:14px;font-weight:600}.list-meta{display:block;margin-top:2px;color:var(--muted);font-size:12.5px}.list-row .list-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end}.card.flush{padding:0}.avatar{display:grid;place-items:center;width:34px;height:34px;flex:0 0 34px;border-radius:50%;background:var(--note-wash);color:var(--note-ink);font-size:12.5px;font-weight:700;letter-spacing:.02em}.dot-state{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--quiet)}.dot-state[data-tone=ok]{background:var(--accent)}.dot-state[data-tone=idle]{background:var(--idle)}.dot-state[data-tone=warn]{background:var(--warn)}.dot-state[data-tone=bad]{background:var(--danger)}.checks.columns{display:grid;gap:0 24px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}.crumb{color:var(--muted);font-size:12.5px;text-decoration:none;white-space:nowrap}.crumb:hover{color:var(--accent-ink)}.versions{display:flex;flex-wrap:wrap;gap:4px}.visit{padding:16px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-lift)}.visit+.visit{margin-top:10px}.visit header{display:flex;align-items:center;gap:12px;margin-bottom:14px}.visit header b{font-size:13.5px}.visit header span{display:block;color:var(--quiet);font-size:12px}.visit header .tag{margin-left:auto}.journey-answers{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;margin:0 0 15px;overflow:hidden;border:1px solid var(--line);border-radius:var(--radius-xs);background:var(--line)}.journey-answer{position:relative;min-width:0;padding:11px 12px 11px 38px;background:var(--surface)}.journey-answer .ico{position:absolute;top:13px;left:12px;width:16px;height:16px;color:var(--quiet)}.journey-answer[data-kind=entry] .ico,.journey-answer[data-kind=outcome] .ico{color:var(--accent)}.journey-answer[data-kind=selection] .ico{color:var(--note-ink)}.journey-answer span,.journey-answer b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.journey-answer span{margin-bottom:3px;color:var(--quiet);font-size:11px;font-weight:650;letter-spacing:.04em;text-transform:uppercase}.journey-answer b{color:var(--text);font-size:13px;font-weight:600}.journey-timeline{margin:0;padding:0 0 0 14px;list-style:none;border-left:1px solid var(--line)}.journey-timeline li{position:relative;display:flex;align-items:baseline;gap:10px;padding:7px 0 7px 5px;font-size:12.5px}.timeline-dot{position:absolute;left:-18px;top:12px;width:7px;height:7px;border-radius:50%;background:var(--accent)}.timeline-dot[data-action=select],.timeline-dot[data-action=open]{background:var(--note-ink)}.timeline-dot[data-action=close]{background:var(--quiet)}.journey-timeline li>div{min-width:0}.journey-timeline li b{display:inline;font-weight:600}.journey-timeline li div span{display:block;color:var(--muted)}.journey-timeline li time{margin-left:auto;padding-left:10px;color:var(--quiet);white-space:nowrap}.visits{max-height:60vh;overflow-y:auto}@media (max-width: 640px){.journey-answers{grid-template-columns:1fr}.journey-timeline li{align-items:flex-start;flex-wrap:wrap}.journey-timeline li time{width:100%;margin:2px 0 0;padding-left:0}}.summary{padding:13px 15px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--surface-lift)}.summary-head{display:flex;align-items:baseline;gap:10px;margin-bottom:9px}.summary-head b{font-size:13px}.summary-head strong{margin-left:auto;font-size:22px;font-weight:650;letter-spacing:-.03em}.summary p{margin:9px 0 0;color:var(--muted);font-size:12.5px}.summary-grid{display:grid;gap:12px;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));margin-top:14px}.table-sub{display:block;color:var(--quiet);font-size:11.5px}.route-arrow{color:var(--quiet)}details summary{cursor:pointer;font-size:12.5px;padding:6px 0}.swatch{position:relative;width:34px;height:22px;flex:0 0 34px;margin-top:1px;border-radius:var(--radius-xs);background:var(--swatch-surface, var(--surface-hi));border:1px solid var(--swatch-hairline, var(--line));overflow:hidden}.swatch i{position:absolute;inset:auto 0 0 0;height:7px;background:var(--swatch-accent, var(--accent))}.group-label{margin:4px 0 2px;color:var(--quiet);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.group+.group{margin-top:14px;padding-top:14px;border-top:1px solid var(--line-soft)}.hero-pins{display:grid;gap:8px;margin-bottom:16px}.hero-pin{display:grid;grid-template-columns:30px minmax(0,1fr) auto auto;align-items:center;gap:10px;min-height:52px;padding:8px 10px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface-lift)}.hero-pin-order{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;background:var(--note-wash);color:var(--note-ink);font-size:12px;font-weight:750}.hero-pin>span:nth-child(2){min-width:0}.hero-pin b,.hero-pin small{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.hero-pin b{font-size:13.5px}.hero-pin small{margin-top:2px;color:var(--quiet);font-size:11.5px}.hero-schedule-list{display:grid;gap:10px}.hero-schedule{position:relative;display:grid;grid-template-columns:100px minmax(0,1fr) auto;align-items:center;gap:16px;padding:15px;overflow:hidden;border:1px solid var(--line);border-radius:11px;background:linear-gradient(135deg,var(--surface-lift),rgba(13,17,23,.74))}.hero-schedule:before{content:"";position:absolute;inset:0 auto 0 0;width:3px;background:var(--quiet);opacity:.4}.hero-schedule[data-enabled=true]:before{background:var(--info);opacity:1}.hero-schedule-time{align-self:stretch;display:flex;flex-direction:column;justify-content:center;padding-right:14px;border-right:1px solid var(--line-soft)}.hero-schedule-time b{color:var(--info-ink);font-size:11px;letter-spacing:.08em;text-transform:uppercase}.hero-schedule-time span{margin-top:4px;color:var(--muted);font:12px/1.4 var(--mono)}.hero-schedule-main{min-width:0}.hero-schedule-title{display:flex;align-items:center;gap:8px}.hero-schedule-title h3{min-width:0;margin:0;overflow:hidden;color:var(--text);font-size:14px;font-weight:650;text-overflow:ellipsis;white-space:nowrap}.hero-schedule-main>p{margin:4px 0 8px;color:var(--muted);font-size:12.5px}.hero-schedule-actions{display:flex;align-items:center;gap:6px}.hero-schedule-dialog{width:min(760px,100%);padding:0;overflow-x:hidden}.hero-schedule-dialog-head{padding:24px 26px 20px;border-bottom:1px solid var(--line);background:radial-gradient(circle at 90% -40%,var(--info-wash),transparent 240px),var(--surface-lift)}.hero-schedule-kicker{color:var(--info-ink);font-size:10.5px;font-weight:750;letter-spacing:.1em;text-transform:uppercase}.hero-schedule-dialog-head h2{margin-top:4px;font-size:21px;letter-spacing:-.025em}.hero-schedule-dialog-head p{margin-bottom:0}.hero-schedule-dialog>.fields,.hero-schedule-dialog>.schedule-days,.hero-schedule-dialog>.schedule-options,.hero-schedule-dialog>.check{margin-right:26px;margin-left:26px}.schedule-frequency{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:20px 26px}.schedule-frequency button{display:block;height:auto;min-height:68px;padding:11px 13px;text-align:left;white-space:normal}.schedule-frequency button b,.schedule-frequency button span{display:block}.schedule-frequency button span{margin-top:4px;color:var(--quiet);font-size:11.5px;font-weight:500}.schedule-frequency button[aria-pressed=true]{border-color:var(--info);background:var(--info-wash);color:var(--info-ink)}.schedule-frequency button[aria-pressed=true] span{color:var(--muted)}.schedule-days{margin-top:18px;padding:16px;border:1px solid var(--line-soft);border-radius:10px;background:var(--surface-lift)}.schedule-days-head{display:flex;align-items:center;gap:12px;margin-bottom:12px}.schedule-days-head>b{font-size:12.5px}.schedule-days-head>div{display:flex;gap:4px;margin-left:auto}.schedule-days-head button{height:28px;padding:0 8px;border-color:transparent;background:none;color:var(--muted);font-size:11.5px}.schedule-day-grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:6px}.schedule-day-grid button{width:100%;height:38px;padding:0;color:var(--muted)}.schedule-day-grid button[aria-pressed=true]{border-color:var(--accent);background:var(--accent-wash);color:var(--accent-ink)}.schedule-options{display:grid;grid-template-columns:minmax(0,2fr) minmax(140px,1fr);align-items:start;gap:22px;margin-top:20px;padding-top:18px;border-top:1px solid var(--line-soft)}.schedule-option-label{display:block;margin-bottom:3px;color:var(--muted);font-size:12px;font-weight:600}.schedule-placement-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px 14px}.hero-schedule-dialog>.check{margin-top:8px}.hero-schedule-dialog>.dialog-actions{margin-top:8px;padding:18px 26px;border-top:1px solid var(--line);background:var(--surface-lift)}@media (max-width: 820px){.hero-pin{grid-template-columns:30px minmax(0,1fr) auto}.hero-pin>button:last-child{grid-column:3}.hero-schedule{grid-template-columns:78px minmax(0,1fr);gap:12px}.hero-schedule-actions{grid-column:1 / -1;padding-top:10px;border-top:1px solid var(--line-soft)}.hero-schedule-actions button{flex:1 1 0}.hero-schedule-dialog-head{padding:22px 20px 18px}.hero-schedule-dialog>.fields,.hero-schedule-dialog>.schedule-days,.hero-schedule-dialog>.schedule-options,.hero-schedule-dialog>.check{margin-right:20px;margin-left:20px}.schedule-frequency{gap:6px;margin:16px 20px}.schedule-frequency button{min-width:0;min-height:62px;padding:9px}.schedule-frequency button span{display:none}.schedule-days-head{align-items:flex-start}.schedule-days-head>div{flex-wrap:wrap;justify-content:flex-end}.schedule-day-grid{gap:4px}.schedule-day-grid button{min-width:0;padding:0 3px;font-size:11px}.schedule-options,.schedule-placement-grid{grid-template-columns:1fr}.hero-schedule-dialog>.dialog-actions{padding:16px 20px max(16px,var(--safe-bottom))}}@media (pointer: coarse) and (min-width: 821px) and (max-width: 1400px){.page-head{margin-bottom:24px}.page-head h1{font-size:26px;line-height:1.2}.page-head p{max-width:72ch;font-size:14px}.card{padding:20px 22px;border-color:#636e7b6b;border-radius:12px;box-shadow:0 1px #ffffff06,0 14px 40px #00000024}.card+.card,.grid+.card,.card+.grid,.grid+.grid{margin-top:20px}.grid{gap:20px}.card-head{align-items:flex-start;margin-bottom:18px}.card-head h2{font-size:15.5px}.card-head p{margin-top:4px;font-size:13px}.card-foot{margin-top:18px;padding-top:16px}.tiles{gap:14px}.tile{min-height:112px;padding:16px 17px;border-radius:12px;background:linear-gradient(145deg,var(--surface-lift),var(--surface))}.tile b{font-size:27px}.filters{gap:12px;padding:16px;border-radius:12px}.filters .field{flex:1 1 calc(50% - 6px);max-width:none}.filters .field.grow{flex-basis:100%}.list-row{min-height:64px;padding:14px 20px}.list-item{min-height:60px;padding:13px 0}.logview{height:min(66dvh,720px)}.logbar-actions{margin-left:0}}@media (max-width: 820px){.page-head-row{display:grid;grid-template-columns:minmax(0,1fr);gap:14px}.page-head-row .page-head-actions{width:100%;margin-left:0}.card{padding:17px 18px;border-radius:11px}.card-head{align-items:flex-start;flex-wrap:wrap}.card-head-actions{width:100%;margin-left:0}.card-foot>button,.card-foot>.btn{flex:1 1 auto}.tile{min-width:0;min-height:104px;padding:14px;background:linear-gradient(145deg,var(--surface-lift),var(--surface))}.tile b{font-size:23px}.tile span{overflow-wrap:anywhere}.filters .field,.filters .field.grow{flex:1 1 100%;max-width:none}.filters .filter-actions{width:100%;margin-left:0}.list-item,.list-row,.list-main{align-items:flex-start}.list-item,.list-row{flex-wrap:wrap}.list-item .list-actions,.list-row .list-actions{width:100%;justify-content:flex-start;padding-left:46px}.kv-row{align-items:flex-start}.dialog{width:100%;max-height:min(88dvh,720px);padding:22px;border-radius:16px}.scrim{align-items:end;padding:12px max(12px,var(--safe-right)) max(12px,var(--safe-bottom)) max(12px,var(--safe-left))}.dialog-actions>button{flex:1 1 0}.bell-panel{position:fixed;top:calc(var(--top) + 8px);right:max(12px,var(--safe-right));left:max(12px,var(--safe-left));width:auto;max-height:calc(100dvh - var(--top) - var(--safe-bottom) - 20px);border-radius:14px}.bell-list{max-height:calc(100dvh - var(--top) - var(--safe-bottom) - 120px)}.toasts{right:max(12px,var(--safe-right));bottom:max(12px,var(--safe-bottom));left:max(12px,var(--safe-left));max-width:none}}@media (pointer: coarse) and (max-width: 1400px){button,.btn{min-width:44px;height:44px;padding-right:15px;padding-left:15px}button[data-size=sm]{min-width:38px;height:38px;padding-right:11px;padding-left:11px}.topbar .rail-toggle,.topbar .bell-button,.topbar .topbar-status,.topbar .account-trigger{width:44px;height:44px;padding:0}.topbar .bell,.topbar .account-menu,.topbar .omni-input{height:44px}.rail a{min-height:46px;padding:10px 12px;border-radius:9px;font-size:14px}.rail-head{min-height:42px;padding:12px}input[type=text],input[type=password],input[type=search],input[type=number],input[type=date],input[type=time],input[type=datetime-local],input[type=url],select{height:44px;font-size:16px}select{background-position:calc(100% - 16px) 20px,calc(100% - 11px) 20px}textarea{min-height:104px;font-size:16px}.check{min-height:52px;padding-top:14px;padding-bottom:14px}.check .switch{width:40px;height:24px;flex-basis:40px}.check .switch:after{width:18px;height:18px}.check input:checked+.switch:after{transform:translate(16px)}.segments{max-width:100%;overflow-x:auto}.segments button{height:38px}.crumb,.crumbs a,.table-row-link{display:inline-flex;align-items:center;min-height:36px}}@media (pointer: fine) and (min-width: 821px) and (max-height: 800px){.page{padding-top:20px;padding-bottom:52px}.page-head{margin-bottom:16px}.card{padding-top:14px;padding-bottom:14px}.card-head{margin-bottom:11px}}@media (hover: none){tbody tr:hover td,.list-row:hover,.bell-item:hover,a.tile:hover{background:inherit}} diff --git a/admin-ui/dist/assets/index-d9286FJI.js b/admin-ui/dist/assets/index-d9286FJI.js new file mode 100644 index 0000000..dfafa1c --- /dev/null +++ b/admin-ui/dist/assets/index-d9286FJI.js @@ -0,0 +1,11 @@ +import{r as p,a as cn,u as is,L as re,b as as,m as Xe,N as _s,O as dn,c as We,B as hn,R as un,d as B,e as mn}from"./router-D9WH5XEU.js";(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))i(r);new MutationObserver(r=>{for(const o of r)if(o.type==="childList")for(const a of o.addedNodes)a.tagName==="LINK"&&a.rel==="modulepreload"&&i(a)}).observe(document,{childList:!0,subtree:!0});function t(r){const o={};return r.integrity&&(o.integrity=r.integrity),r.referrerPolicy&&(o.referrerPolicy=r.referrerPolicy),r.crossOrigin==="use-credentials"?o.credentials="include":r.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function i(r){if(r.ep)return;r.ep=!0;const o=t(r);fetch(r.href,o)}})();var Us={exports:{}},He={};/** + * @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 pn=p,xn=Symbol.for("react.element"),jn=Symbol.for("react.fragment"),vn=Object.prototype.hasOwnProperty,gn=pn.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,bn={key:!0,ref:!0,__self:!0,__source:!0};function Vs(s,n,t){var i,r={},o=null,a=null;t!==void 0&&(o=""+t),n.key!==void 0&&(o=""+n.key),n.ref!==void 0&&(a=n.ref);for(i in n)vn.call(n,i)&&!bn.hasOwnProperty(i)&&(r[i]=n[i]);if(s&&s.defaultProps)for(i in n=s.defaultProps,n)r[i]===void 0&&(r[i]=n[i]);return{$$typeof:xn,type:s,key:o,ref:a,props:r,_owner:gn.current}}He.Fragment=jn;He.jsx=Vs;He.jsxs=Vs;Us.exports=He;var e=Us.exports,Bs,ms=cn;Bs=ms.createRoot,ms.hydrateRoot;const Ws={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",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 J({name:s,className:n}){const t=Ws[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 ze({name:s,tone:n}){return Ws[s]?e.jsx("span",{className:"glyph","data-tone":n,children:e.jsx(J,{name:s})}):null}const Ce=[{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:"Memby 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:"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:"Server logs",title:"Server 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:"recommendations",path:"/admin/recommendations",label:"For You",title:"For You",intro:"The prepared pools personalised rows are drawn from.",icon:"sparkle"},{id:"ratings",path:"/admin/ratings",label:"Movie ratings",title:"Movie ratings",intro:"Optional MDBList scores on films and shows.",icon:"star"},{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:"imports",path:"/admin/imports",label:"Imports",title:"Imports",intro:"Catalogue synchronisation history.",icon:"database"},{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",path:"/admin/integrations",label:"Integrations",title:"Integrations",intro:"Send administrative events to Discord and, in time, elsewhere.",icon:"plug"}]},{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 the household has been looking for, and what it 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"}]}],fn=Ce.flatMap(s=>s.items),yn=Ce.flatMap(s=>s.items.filter(n=>!n.path.includes(":")).map(n=>({...n,group:s.label??""})));class ps extends Error{constructor(n,t){super(n),this.status=t,this.name="ApiError"}}const wn=5*60*1e3;let Hs=Date.now();for(const s of["pointerdown","pointermove","keydown","wheel","scroll"])window.addEventListener(s,()=>{Hs=Date.now()},{passive:!0});const kn=()=>Date.now()-Hs({}));throw new ps(i.error??`Request failed (${t.status})`,t.status)}if(t.status!==204)return await t.json()}function Te(s){const n=new URLSearchParams;for(const[i,r]of Object.entries(s))r==null||r===""||r===!1||n.set(i,String(r));const t=n.toString();return t?`?${t}`:""}const F={get:s=>qe(s),post:(s,n)=>qe(s,{method:"POST",body:n===void 0?void 0:JSON.stringify(n)}),put:(s,n)=>qe(s,{method:"PUT",body:n===void 0?void 0:JSON.stringify(n)}),del:s=>qe(s,{method:"DELETE"})},Sn=3e4,zs=p.createContext(null);function Cn({children:s}){var h;const[n,t]=p.useState(),[i,r]=p.useState(!1),[o,a]=p.useState(""),[d,c]=p.useState(""),[l,v]=p.useState(!0),g=p.useRef(0),u=p.useCallback(async()=>{const j=++g.current;try{const b=await F.get("/admin/api/status");if(j!==g.current)return;t(b),r(!0),c("")}catch(b){if(j!==g.current)return;r(!1),c(b instanceof Error?b.message:String(b))}finally{j===g.current&&(a(new Date().toISOString()),v(!1))}},[]),m=p.useCallback(async j=>{var b;await F.post("/admin/api/maintenance",{enabled:j,message:((b=n==null?void 0:n.maintenance)==null?void 0:b.message)??""}),await u()},[u,(h=n==null?void 0:n.maintenance)==null?void 0:h.message]);p.useEffect(()=>{u();let j;const b=()=>{window.clearInterval(j),j=document.hidden?void 0:window.setInterval(()=>void u(),Sn)},k=()=>{b(),document.hidden||u()};return b(),document.addEventListener("visibilitychange",k),()=>{window.clearInterval(j),document.removeEventListener("visibilitychange",k)}},[u]);const f=p.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:i,checkedAt:o,error:d,loading:l,reload:u,setMaintenance:m}},[n,i,o,d,l,u,m]);return e.jsx(zs.Provider,{value:f,children:s})}function oe(){const s=p.useContext(zs);if(!s)throw new Error("useGateway used outside GatewayProvider");return s}function Mn(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 En(){const s=is(),{status:n}=oe(),[t,i]=p.useState(!1),[r,o]=p.useState(""),[a,d]=p.useState(0),c=p.useRef(null),l=p.useRef(null),v=p.useMemo(()=>{const u=r.trim().toLowerCase();return[...yn,...((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:u?Mn(f,u):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}))},[r,n]);p.useEffect(()=>d(0),[r]),p.useEffect(()=>{const u=m=>{var f;(f=c.current)!=null&&f.contains(m.target)||i(!1)};return document.addEventListener("pointerdown",u),()=>document.removeEventListener("pointerdown",u)},[]),p.useEffect(()=>{const u=m=>{var h,j;if(m.key!=="S"||!m.shiftKey||m.ctrlKey||m.metaKey||m.altKey)return;const f=document.activeElement;f&&(f.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(f.tagName))||(m.preventDefault(),(h=l.current)==null||h.focus(),(j=l.current)==null||j.select())};return window.addEventListener("keydown",u),()=>window.removeEventListener("keydown",u)},[]);const g=u=>{var m;i(!1),o(""),(m=l.current)==null||m.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(J,{name:"search"}),e.jsx("input",{ref:l,type:"search",value:r,placeholder:"Search pages, users and devices…","aria-label":"Search pages, users and devices","aria-expanded":t,onFocus:()=>i(!0),onChange:u=>{o(u.target.value),i(!0)},onKeyDown:u=>{var m;if(u.key==="Escape")o(""),i(!1),(m=l.current)==null||m.blur();else if(u.key==="ArrowDown")u.preventDefault(),d(f=>Math.min(f+1,v.length-1));else if(u.key==="ArrowUp")u.preventDefault(),d(f=>Math.max(f-1,0));else if(u.key==="Enter"){const f=v[a];if(!f)return;u.preventDefault(),g(f.item.path)}}}),e.jsx("span",{className:"omni-key",children:"⇧S"})]}),t?e.jsx("div",{className:"omni-panel",role:"listbox",children:v.length===0?e.jsx("p",{className:"empty",children:"No pages, users or devices match that search."}):v.map((u,m)=>e.jsxs("a",{className:m===a?"omni-item on":"omni-item",href:u.item.path,role:"option","aria-selected":m===a,onPointerEnter:()=>d(m),onClick:f=>{f.preventDefault(),g(u.item.path)},children:[u.item.icon?e.jsx(J,{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(),P=s=>s?new Date(s).toLocaleString():"—";function Ne(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 ke(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 es(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 Ae(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(),Ve=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 i=Math.floor(t/60);if(i<60)return`${i} min ago`;const r=Math.floor(i/60);if(r<24)return`${r}h ago`;const o=Math.floor(r/24);return o<30?`${o}d ago`:new Date(s).toLocaleDateString()}const Gs=15*60*1e3,An=3*60*60*1e3,$e=s=>!!s&&Date.now()-new Date(s).getTime(){var b;try{const k=await F.get(`/admin/api/notifications?limit=${js}`);t(k.events),r(k.unread),a(k.types),g.current=Math.max(g.current,((b=k.events[0])==null?void 0:b.id)??0),v("")}catch(k){v(k instanceof Error?k.message:String(k))}},[]),m=p.useCallback(b=>{g.current=Math.max(g.current,b.id),t(k=>k.some(x=>x.id===b.id)?k:[b,...k].sort((x,y)=>y.id-x.id).slice(0,js)),b.readAt||r(k=>k+1),a(k=>k.some(x=>x.type===b.type)?k.map(x=>x.type===b.type?{...x,count:x.count+1}:x):[...k,{type:b.type,count:1}])},[]);p.useEffect(()=>{u()},[u]),p.useEffect(()=>{let b=null,k,x=!1;return(()=>{x||(b=new EventSource(`/admin/api/notifications/stream?after=${g.current}`),b.addEventListener("open",()=>{c(!0),window.clearInterval(k),k=void 0}),b.addEventListener("admin",R=>{try{m(JSON.parse(R.data))}catch{}}),b.addEventListener("error",()=>{c(!1),k===void 0&&(k=window.setInterval(()=>void u(),Rn))}))})(),()=>{x=!0,b==null||b.close(),window.clearInterval(k)}},[m,u]);const f=p.useCallback(async b=>{const k=b.filter(x=>x>0);if(k.length!==0){t(x=>x.map(y=>k.includes(y.id)&&!y.readAt?{...y,readAt:new Date().toISOString()}:y));try{const x=await F.post("/admin/api/notifications/read",{ids:k});r(x.unread)}catch{u()}}},[u]),h=p.useCallback(async()=>{r(0),t(b=>b.map(k=>k.readAt?k:{...k,readAt:new Date().toISOString()}));try{const b=await F.post("/admin/api/notifications/read",{all:!0});r(b.unread)}catch{u()}},[u]),j=p.useMemo(()=>({events:n,unread:i,types:o,connected:d,error:l,markRead:f,markAllRead:h,reload:u}),[n,i,o,d,l,f,h,u]);return e.jsx(Zs.Provider,{value:j,children:s})}function ls(){const s=p.useContext(Zs);if(!s)throw new Error("useNotifications used outside NotificationProvider");return s}function Js(s){return s.severity==="error"?"bad":s.severity==="warning"?"warn":"info"}function Ys(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 vs(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 gs=99;function Tn(){const{events:s,unread:n,connected:t,markRead:i,markAllRead:r}=ls(),[o,a]=p.useState(!1),d=p.useRef(null);return p.useEffect(()=>{const c=v=>{var g;(g=d.current)!=null&&g.contains(v.target)||a(!1)},l=v=>{v.key==="Escape"&&a(!1)};return document.addEventListener("pointerdown",c),document.addEventListener("keydown",l),()=>{document.removeEventListener("pointerdown",c),document.removeEventListener("keydown",l)}},[]),p.useEffect(()=>{if(!o)return;const c=s.filter(l=>!l.readAt).map(l=>l.id);c.length>0&&i(c)},[o]),e.jsxs("div",{className:"bell",ref:d,"data-open":o||void 0,children:[e.jsxs("button",{type:"button",className:"bell-button","aria-label":n>0?`Activity, ${n} unread`:"Activity","aria-expanded":o,onClick:()=>a(c=>!c),children:[e.jsx(J,{name:"bell"}),n>0?e.jsx("span",{className:"bell-badge",children:n>gs?`${gs}+`:n}):null]}),o?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 r(),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 l=e.jsxs(e.Fragment,{children:[e.jsx(ze,{name:Ys(c.type),tone:Js(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(re,{className:"bell-item","data-unread":!c.readAt||void 0,to:c.link,onClick:()=>a(!1),children:l},c.id):e.jsx("div",{className:"bell-item","data-unread":!c.readAt||void 0,children:l},c.id)})}),e.jsx("div",{className:"bell-foot",children:e.jsx(re,{to:"/admin/activity",onClick:()=>a(!1),children:"All activity"})})]}):null]})}function U({title:s,intro:n,actions:t,crumbs:i,icon:r}){var d;const o=as(),a=r??((d=fn.find(c=>Xe({path:c.path,end:!0},o.pathname)))==null?void 0:d.icon);return 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:[a?e.jsx("span",{className:"page-head-icon","aria-hidden":"true",children:e.jsx(J,{name:a})}):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 T({title:s,intro:n,icon:t,tone:i,actions:r,footer:o,children:a}){return e.jsxs("section",{className:"card",children:[s?e.jsxs("div",{className:"card-head",children:[t?e.jsx(ze,{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]}),r?e.jsx("div",{className:"card-head-actions",children:r}):null]}):null,a,o?e.jsx("div",{className:"card-foot",children:o}):null]})}function he({cols:s,children:n}){return e.jsx("div",{className:"grid","data-cols":s,children:n})}function le({tiles:s}){return e.jsx("div",{className:"tiles",children:s.map(n=>e.jsxs("div",{className:"tile",children:[n.icon?e.jsx(ze,{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 M({children:s,tone:n}){return e.jsx("span",{className:"tag","data-tone":n,children:s})}function ie({children:s,tone:n}){return e.jsx("span",{className:"chip","data-tone":n,children:s})}function Y({children:s}){return e.jsx("p",{className:"empty",children:s})}function ae({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 je({children:s,tone:n}){return e.jsx("p",{className:"note","data-tone":n,children:s})}function $({children:s,onClick:n,variant:t,size:i,disabled:r,busy:o,icon:a,type:d="button",title:c}){return e.jsxs("button",{type:d,className:"","data-variant":t,"data-size":i,disabled:r||o,onClick:n,title:c,children:[o?e.jsx("span",{className:"spinner"}):a?e.jsx(J,{name:a}):null,s]})}function q({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 z({label:s,hint:n,checked:t,onChange:i,disabled:r}){return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:t,disabled:r,onChange:o=>i(o.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 Be({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 Z({children:s}){return e.jsx("div",{className:"table-wrap",children:s})}function Qs({data:s,labelOf:n,valueOf:t,toneOf:i,title:r}){if(s.length===0)return e.jsx(Y,{children:"Nothing in this window."});const o=s.map(d=>t(d)),a=Math.max(1,...o);return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"bars",children:s.map((d,c)=>{const l=t(d);return e.jsx("div",{className:"bar","data-tone":i==null?void 0:i(d),"data-empty":l===0||void 0,style:{height:`${Math.max(2,l/a*100)}%`},title:r?r(d):`${n(d,c)}: ${l}`},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 In({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 V({message:s,onDismiss:n}){return s?e.jsxs("div",{className:"banner",role:"alert",children:[e.jsx(J,{name:"alert"}),e.jsx("span",{children:s}),n?e.jsx("button",{type:"button",onClick:n,"aria-label":"Dismiss",children:e.jsx(J,{name:"close"})}):null]}):null}function W({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 xe({title:s,body:n,confirmLabel:t="Confirm",destructive:i,busy:r,onConfirm:o,onCancel:a}){const d=p.useId(),c=p.useRef(null);return p.useEffect(()=>{var v;(v=c.current)==null||v.focus();const l=g=>{g.key==="Escape"&&a()};return document.addEventListener("keydown",l),()=>document.removeEventListener("keydown",l)},[a]),e.jsx("div",{className:"scrim",onPointerDown:l=>l.target===l.currentTarget&&a(),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:a,variant:"quiet",children:"Cancel"}),e.jsx($,{onClick:o,variant:i?"danger":"primary",busy:r,children:t})]})]})})}function ss({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({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 Ln({open:s,onNavigate:n}){const{unread:t}=ls(),i=as(),[r,o]=p.useState(()=>{try{return JSON.parse(localStorage.getItem("memby-admin-nav")??"{}")}catch{return{}}}),a=c=>{try{localStorage.setItem("memby-admin-nav",JSON.stringify(c))}catch{}};p.useEffect(()=>{const c=Ce.find(l=>l.items.some(v=>Xe({path:v.path,end:!0},i.pathname)));c&&o(l=>{const v={...l};return Ce.forEach(g=>{g.collapsible!==!1&&(v[g.id]=g.id!==c.id)}),a(v),v})},[i.pathname]);const d=(c,l=!1)=>{o(v=>{const g={...v},u=!(v[c]??l);return Ce.forEach(m=>{m.collapsible!==!1&&(g[m.id]=m.id===c?u:!0)}),a(g),g})};return e.jsx("nav",{className:"rail",id:"rail","data-open":s||void 0,"aria-label":"Console sections",children:Ce.map(c=>{const l=c.items.filter(m=>!m.hidden);if(l.length===0)return null;const v=l.some(m=>Xe({path:m.path,end:!0},i.pathname)),g=c.collapsible!==!1,u=v||!g||!(r[c.id]??c.defaultCollapsed??!1);return e.jsxs("div",{className:"rail-group",children:[c.label&&g?e.jsxs("button",{type:"button",className:"rail-head","aria-expanded":u,onClick:()=>d(c.id,c.defaultCollapsed),children:[c.label,e.jsx(J,{name:"caret",className:"ico caret"})]}):c.label?e.jsx("div",{className:"rail-head rail-head-static",children:c.label}):null,u?l.map(m=>e.jsx(_s,{to:m.path,end:m.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:[m.icon?e.jsx(J,{name:m.icon}):null,m.label,m.badge==="notifications"&&t>0?e.jsx("span",{className:"rail-badge",children:t>99?"99+":t}):null]})},m.id)):null]},c.id)})})}function qn(){var R,S,H;const{version:s,currentUser:n,online:t,loading:i,status:r,setMaintenance:o}=oe(),[a,d]=p.useState(!1),[c,l]=p.useState(!1),[v,g]=p.useState(!1),[u,m]=p.useState(!1),f=as(),h=!!((R=r==null?void 0:r.maintenance)!=null&&R.enabled),j=!!((S=r==null?void 0:r.quietTime)!=null&&S.active),b=p.useRef(null),k=((H=Array.from(n.trim())[0])==null?void 0:H.toLocaleUpperCase("en-NZ"))||"A",x=r&&t&&!h&&!j?"ok":r||!i?"bad":"checking",y=async()=>{if(!(v||!r)){g(!0);try{await o(!h)}finally{g(!1)}}};return p.useEffect(()=>d(!1),[f.pathname]),p.useEffect(()=>{if(!c)return;const E=G=>{var ee;(ee=b.current)!=null&&ee.contains(G.target)||l(!1)},I=G=>{G.key==="Escape"&&l(!1)};return document.addEventListener("mousedown",E),window.addEventListener("keydown",I),()=>{document.removeEventListener("mousedown",E),window.removeEventListener("keydown",I)}},[c]),p.useEffect(()=>{if(!a)return;const E=document.body.style.overflow;document.body.style.overflow="hidden";const I=G=>{G.key==="Escape"&&d(!1)};return window.addEventListener("keydown",I),()=>{document.body.style.overflow=E,window.removeEventListener("keydown",I)}},[a]),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":a,"aria-controls":"rail",onClick:()=>d(E=>!E),children:e.jsx(J,{name:"menu"})}),e.jsx("div",{className:"topbar-spacer"}),e.jsxs("div",{className:"topbar-tools",children:[e.jsx(En,{}),e.jsxs("span",{className:"topbar-version",children:["gateway ",s||"unknown"]}),e.jsx("button",{type:"button",className:"topbar-status","data-tone":x,"aria-pressed":h,disabled:!r||!t||v||j,title:r?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":r&&t?"Memby is online. Take it offline":i?"Checking Memby status":"Memby is not responding",onClick:()=>h?void y():m(!0),children:e.jsx("span",{className:"dot","aria-hidden":"true"})}),e.jsx(Tn,{}),e.jsxs("div",{className:"account-menu","data-open":c||void 0,ref:b,children:[e.jsxs("button",{type:"button",className:"account-trigger","aria-haspopup":"menu","aria-expanded":c,"aria-label":`Signed in as ${n}`,onClick:()=>l(E=>!E),children:[e.jsx("span",{className:"account-avatar","aria-hidden":"true",children:k}),e.jsx("span",{className:"account-name",children:n}),e.jsx(J,{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:k}),e.jsxs("span",{children:[e.jsx("small",{children:"Signed in as"}),e.jsx("b",{children:n})]})]}),e.jsxs(_s,{to:"/admin/settings",role:"menuitem",onClick:()=>l(!1),children:[e.jsx(J,{name:"sliders"}),"Gateway settings"]}),e.jsx("form",{method:"post",action:"/admin/logout",children:e.jsxs("button",{type:"submit",role:"menuitem",children:[e.jsx(J,{name:"logout"}),"Log out"]})})]}):null]})]})]}),a?e.jsx("button",{type:"button",className:"rail-scrim","aria-label":"Close sections",onClick:()=>d(!1)}):null,e.jsx(Ln,{open:a,onNavigate:()=>d(!1)}),e.jsx("main",{className:"page",id:"main",children:e.jsx(dn,{})}),u?e.jsx(xe,{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:v,onConfirm:()=>{m(!1),y()},onCancel:()=>m(!1)}):null]})}const en=p.createContext(null),Dn=5e3;function Fn({children:s}){const[n,t]=p.useState([]),i=p.useRef(1),r=p.useCallback(c=>{t(l=>l.filter(v=>v.id!==c))},[]),o=p.useCallback((c,l="ok")=>{const v=i.current++;t(g=>[...g,{id:v,message:c,tone:l}]),window.setTimeout(()=>r(v),Dn)},[r]),a=p.useCallback(async(c,l)=>{try{const v=await c();return l&&o(l,"ok"),v}catch(v){o(v instanceof Error?v.message:String(v),"bad");return}},[o]),d=p.useMemo(()=>({show:o,wrap:a}),[o,a]);return e.jsxs(en.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(J,{name:c.tone==="bad"?"alert":"check"}),e.jsx("span",{children:c.message}),e.jsx("button",{type:"button",onClick:()=>r(c.id),"aria-label":"Dismiss",children:e.jsx(J,{name:"close"})})]},c.id))})]})}function ne(){const s=p.useContext(en);if(!s)throw new Error("useToast used outside ToastProvider");return s}function Q(s,n={}){const{pollMs:t,enabled:i=!0}=n,[r,o]=p.useState(),[a,d]=p.useState(""),[c,l]=p.useState(i),[v,g]=p.useState(!1),u=p.useRef(0),m=p.useRef(!1),f=p.useCallback(async()=>{if(!i)return;const h=++u.current;m.current&&g(!0);try{const j=await F.get(s);if(h!==u.current)return;o(j),d(""),m.current=!0}catch(j){if(h!==u.current)return;d(j instanceof Error?j.message:String(j))}finally{h===u.current&&(l(!1),g(!1))}},[s,i]);return p.useEffect(()=>(m.current=!1,l(!0),f(),()=>{u.current+=1}),[f]),p.useEffect(()=>{if(!t||!i)return;let h;const j=()=>{window.clearInterval(h),h=document.hidden?void 0:window.setInterval(()=>void f(),t)},b=()=>{j(),document.hidden||f()};return j(),document.addEventListener("visibilitychange",b),()=>{window.clearInterval(h),document.removeEventListener("visibilitychange",b)}},[t,i,f]),{data:r,error:a,loading:c,refreshing:v,reload:f,set:o}}function X(){const[s,n]=p.useState(null),t=p.useRef(!0);p.useEffect(()=>()=>{t.current=!1},[]);const i=p.useCallback(async(r,o)=>{n(r);try{return await o(),!0}finally{t.current&&n(null)}},[]);return{busy:s,run:i}}function Pn(){var j,b,k,x,y,R;const{status:s,error:n,loading:t}=oe(),i=Q("/admin/api/runtime",{pollMs:3e4}),r=Q("/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(V,{message:n}),e.jsx(W,{})]});const o=s.features??{features:[],revision:0,safeMode:!1},a=o.features??[],d=s.clients??[],c=d.filter(S=>$e(S.lastSeen)).length,l=s.updatePolicy??{},v=!!l.minimumVersion&&l.minimumVersion===l.latestVersion,g=s.playbackPolicy,u=s.mdblist,m=s.forYou,f=(s.runs??[]).slice(0,5),h=i.data;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Overview",intro:"What the gateway is doing right now."}),e.jsx(V,{message:n}),e.jsx(le,{tiles:[{label:"items in the library",value:w(s.library.total),icon:"library",tone:"data"},{label:"people signed in",value:w((s.requestUsers??[]).length),icon:"people",tone:"note"},{label:`devices · ${c} active now`,value:w(d.length),icon:"tv",tone:"info"},{label:`visits today · ${((j=r.data)==null?void 0:j.lastWeek.visits)??0} this time last week`,value:w((b=r.data)==null?void 0:b.today.visits),icon:"overview",tone:"data"},{label:`viewers today · ${((k=r.data)==null?void 0:k.lastWeek.viewers)??0} this time last week`,value:w((x=r.data)==null?void 0:x.today.viewers),icon:"people",tone:"note"},{label:"optional features on",value:`${a.filter(S=>S.enabled).length} / ${a.length}`,icon:"sliders",tone:"ok"},{label:"last import",value:P(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsxs(he,{cols:"2",children:[e.jsx(T,{title:"What televisions are being told",intro:"The answers the gateway is giving every set right now.",icon:"tv",tone:"info",children:e.jsx(ss,{rows:[{label:"Availability",value:(y=s.maintenance)!=null&&y.enabled?e.jsx(M,{tone:"bad",children:"offline for maintenance"}):(R=s.quietTime)!=null&&R.active?e.jsx(M,{tone:"warn",children:"quiet time active"}):e.jsx(M,{tone:"ok",children:"online"})},{label:"Feature control plane",value:o.safeMode?e.jsx(M,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(M,{tone:"ok",children:["revision r",w(o.revision)]})},{label:"App update prompt",value:l.enabled?e.jsxs(M,{tone:v?"warn":"ok",children:[v?"required · ":"optional · ",l.latestVersion]}):e.jsx(M,{children:"off"})},{label:"Catalogue import",value:s.syncRunning?e.jsx(M,{tone:"warn",children:"running"}):e.jsxs(M,{children:["every ",s.syncEvery]})},{label:"Playback preroll",value:(g==null?void 0:g.prerollEnabled)===!1?e.jsx(M,{children:"off"}):e.jsxs(M,{tone:"ok",children:[((g==null?void 0:g.prerollDurationMs)??6500)/1e3,"s"]})}]})}),e.jsx(T,{title:"Services",intro:"The services this gateway leans on, and whether they answered.",icon:"wrench",tone:"note",children:e.jsx(ss,{rows:[{label:"Movies (Radarr)",value:s.radarrReady?e.jsx(M,{tone:"ok",children:"ready"}):e.jsx(M,{children:"not configured"})},{label:"Series (Sonarr)",value:s.sonarrReady?e.jsx(M,{tone:"ok",children:"ready"}):e.jsx(M,{children:"not configured"})},{label:"MDBList ratings",value:u!=null&&u.enabled?e.jsxs(M,{tone:"ok",children:[w(u.cachedTitles)," titles stored"]}):e.jsx(M,{children:u!=null&&u.apiKeyConfigured?"off · key saved":"off · no key"})},{label:"For You pools",value:s.forYouRunning?e.jsx(M,{tone:"warn",children:"rebuilding"}):e.jsxs(M,{children:[w((m==null?void 0:m.candidates)??0)," ranked candidates"]})},{label:"Recommendation profiles",value:e.jsx("span",{className:"mono",children:w((m==null?void 0:m.profiles)??0)})}]})})]}),e.jsxs(he,{cols:"2",children:[e.jsx(T,{title:"Latest imports",intro:"The last few catalogue synchronisations.",icon:"sync",tone:"data",actions:e.jsx(re,{to:"/admin/imports",children:"All imports"}),children:e.jsx(Z,{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(ae,{columns:4,children:"No imports have run yet."}):f.map(S=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(S.startedAt)}),e.jsx("td",{children:S.kind}),e.jsx("td",{children:e.jsx(M,{tone:S.status==="success"?"ok":S.status==="running"?"warn":"bad",children:S.status})}),e.jsx("td",{className:"num",children:w(S.itemsUpserted)})]},S.id||S.startedAt))})]})})}),e.jsx(T,{title:"Process",intro:"The container the gateway is served from.",icon:"chip",tone:"info",children:h?e.jsxs(e.Fragment,{children:[e.jsx(Xs,{tiles:[{label:"goroutines",value:w(h.goroutines)},{label:"heap in use",value:Ae(h.heapInuse)},{label:"reserved",value:Ae(h.sys)},{label:"collections",value:w(h.numGc)}]}),e.jsxs("p",{className:"hint",children:["Next collection at ",Ae(h.nextGc)," · memory limit"," ",h.memoryLimit>0&&h.memoryLimit`/admin/api/notifications${Te({days:r,type:a,severity:c,unread:v,limit:f,offset:u*f})}`,[r,a,c,v,u]),{data:j,error:b,loading:k,reload:x}=Q(h),y=async()=>{await t(),await x()};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 y(),icon:"check",children:"Mark all read"}):void 0}),e.jsx(V,{message:b}),e.jsx(le,{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(Be,{value:r,options:On.map(R=>({value:R.value,label:R.label})),onChange:R=>{o(R),m(0)}})}),e.jsx(q,{label:"Kind",children:e.jsxs("select",{value:a,onChange:R=>{d(R.target.value),m(0)},children:[e.jsx("option",{value:"",children:"Everything"}),((j==null?void 0:j.types)??[]).map(R=>e.jsxs("option",{value:R.type,children:[vs(R.type)," (",R.count,")"]},R.type))]})}),e.jsx(q,{label:"Severity",children:e.jsxs("select",{value:c,onChange:R=>{l(R.target.value),m(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:v?"unread":"",onChange:R=>{g(R.target.value==="unread"),m(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:()=>{x(),i()},children:"Refresh"})})]}),k?e.jsx(W,{}):e.jsx(T,{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:u===0,onClick:()=>m(u-1),children:"Newer"}),e.jsx($,{size:"sm",disabled:(u+1)*f>=((j==null?void 0:j.total)??0),onClick:()=>m(u+1),children:"Older"})]}):void 0,children:e.jsx(Z,{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(ae,{columns:6,children:"Nothing has happened in this window."}):j==null?void 0:j.events.map(R=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(R.occurredAt),children:be(R.occurredAt)}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(ze,{name:Ys(R.type),tone:Js(R)}),vs(R.type)]})}),e.jsxs("td",{children:[e.jsx("b",{children:R.title}),R.summary?e.jsx("div",{className:"muted",children:R.summary}):null]}),e.jsx("td",{className:"muted nowrap",children:R.actor||"—"}),e.jsx("td",{className:"muted nowrap",children:R.target||"—"}),e.jsxs("td",{className:"nowrap",children:[R.readAt?null:e.jsx(M,{tone:"ok",children:"new"}),R.link?e.jsx(re,{className:"table-row-link",to:R.link,children:"Open"}):null]})]},R.id))})]})})})]})}function Un(){const{data:s,error:n,loading:t}=Q("/admin/api/accounts",{pollMs:6e4}),i=(s==null?void 0:s.accounts)??[],r=i.flatMap(l=>l.devices??[]),o=i.filter(l=>{var v;return(v=l.recommendations)==null?void 0:v.completed}).length,a=i.filter(l=>{var v,g;return((v=l.recommendations)==null?void 0:v.prompted)&&!((g=l.recommendations)!=null&&g.completed)}).length,d=i.filter(l=>{var v;return(v=l.watchTime)==null?void 0:v.matched}),c=d.reduce((l,v)=>{var g;return l+(((g=v.watchTime)==null?void 0:g.weekMs)??0)},0);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Memby users",intro:"Who uses Memby, and the devices they are signed in on."}),e.jsx(V,{message:n}),e.jsx(je,{tone:"info",children:"This is the Memby user list, not the Emby user directory. A person appears here only after signing in to the Memby app. Removing access signs their Memby devices out and does not delete or change their Emby account."}),t?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"Memby users",value:w(i.length),icon:"people",tone:"note"},{label:"signed-in devices",value:w(r.length),icon:"tv",tone:"info"},{label:"active in the last quarter hour",value:w(r.filter(l=>$e(l.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"recommendation setups completed",value:w(o),icon:"check",tone:"ok"},{label:"setup prompts queued",value:w(a),icon:"sparkle",tone:"note"},...d.length?[{label:"watched by the household this week",value:ke(c),icon:"pulse",tone:"data"}]:[]]}),e.jsx("section",{className:"card flush",children:i.length===0?e.jsx(Y,{children:"No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here."}):i.map(l=>{var h,j;const v=l.devices??[],g=v.filter(b=>$e(b.lastSeen)).length,u=(h=l.recommendations)!=null&&h.completed?{label:"personalised",tone:"ok"}:(j=l.recommendations)!=null&&j.prompted?{label:"prompt queued",tone:"warn"}:{label:"not invited",tone:void 0},m=rs(l.lastSeen),f=l.watchTime;return e.jsxs(re,{className:"list-row",to:`/admin/accounts/${encodeURIComponent(l.id)}`,children:[e.jsxs("span",{className:"list-main",children:[e.jsx("span",{className:"avatar",children:l.initials||Ks(l.username)}),e.jsxs("span",{children:[e.jsxs("span",{className:"list-title",children:[l.username||"Unnamed user",e.jsx("span",{className:"dot-state","data-tone":m.tone,title:m.label})]}),e.jsxs("span",{className:"list-meta",children:[w(v.length)," device",v.length===1?"":"s",g?` · ${g} active now`:""," · last seen ",P(l.lastSeen),f!=null&&f.matched?` · watched ${ke(f.weekMs)} this week, ${ke(f.monthMs)} this month`:""]})]})]}),e.jsxs("span",{className:"list-actions",children:[f!=null&&f.matched?e.jsx(M,{tone:"data",children:ke(f.weekMs)}):null,e.jsx(M,{tone:u.tone,children:u.label}),e.jsx("span",{className:"crumb",children:"Manage"})]})]},l.id)})})]})]})}function Je(s){const n=String(s??"").replace("#","");return n.length!==8?`#${n}`:`#${n.slice(2)}${n.slice(0,2)}`}function Vn(){var ue;const{userId:s=""}=We(),n=is(),{wrap:t}=ne(),{busy:i,run:r}=X(),o=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:a,error:d,loading:c,reload:l}=Q("/admin/api/accounts",{pollMs:3e4}),[v,g]=p.useState(null),[u,m]=p.useState(null),[f,h]=p.useState(null),[j,b]=p.useState(null),[k,x]=p.useState(null),y=((a==null?void 0:a.accounts)??[]).find(A=>A.id===s),R=(a==null?void 0:a.catalogue)??[],S=(a==null?void 0:a.themes)??[];p.useEffect(()=>{var A;v===null&&y&&g({...((A=y.settings)==null?void 0:A.preferences)??{}})},[y,v]),p.useEffect(()=>{f===null&&y&&h({...y.notifications})},[y,f]),p.useEffect(()=>{if(u!==null||!y)return;const A=y.themes??[];m(A.length===0?S.map(L=>L.id):A)},[y,u,S]);const H=p.useMemo(()=>{const A=[];for(const L of R){let N=A.find(D=>D.name===L.area);N||A.push(N={name:L.area,definitions:[]}),N.definitions.push(L)}return A},[R]),E=(A,L,N,D)=>r(A,async()=>{const _=await t(L,N);b(null),_!==void 0&&(D==null||D()),await l()});if(c)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(re,{to:"/admin/accounts",children:"← All users"})}),e.jsx(W,{})]});if(!y)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(re,{to:"/admin/accounts",children:"← All users"})}),e.jsx(V,{message:d}),e.jsx(T,{children:e.jsx(Y,{children:"This user is no longer signed in to Memby."})})]});const I=y.devices??[],G=I.filter(A=>$e(A.lastSeen)).length,ee=y.settings??{},se=y.recommendations??{};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:y.username||"Unnamed user",intro:`Memby user · ${w(I.length)} device${I.length===1?"":"s"} · last seen ${P(y.lastSeen)}`,crumbs:e.jsx(re,{to:"/admin/accounts",children:"← All users"}),actions:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"avatar",children:y.initials||Ks(y.username)}),G?e.jsxs(M,{tone:"ok",children:[G," active now"]}):e.jsx(M,{children:"idle"}),e.jsx(ie,{children:y.id})]})}),e.jsx(V,{message:d}),e.jsxs(he,{cols:"wide",children:[e.jsx(T,{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:I.length===0?e.jsx(Y,{children:"No devices are signed in to this user."}):e.jsx("div",{className:"list",children:I.map(A=>{const L=rs(A.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":L.tone,title:L.label})," ",e.jsx(re,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(A.id)}`,children:A.name||"Memby TV"})]}),e.jsxs("p",{children:[A.version?`Memby ${A.version}`:"Legacy Memby client"," · ",L.label," · last seen ",P(A.lastSeen)," · signed in ",P(A.signedInAt)]}),(A.versions??[]).length>0?e.jsx("div",{className:"chips",children:(A.versions??[]).map(N=>e.jsxs(ie,{tone:N.version===A.version?"ok":void 0,children:[N.version,N.version===A.version?" · now":""]},N.version))}):null]}),e.jsxs("div",{className:"list-actions",children:[e.jsx($,{size:"sm",disabled:!A.id,onClick:()=>x({id:A.id,name:A.name}),children:"Rename"}),e.jsx($,{size:"sm",variant:"danger",disabled:!A.id,onClick:()=>b({kind:"remove-device",deviceId:A.id,name:A.name}),children:"Sign out"})]})]},A.id||A.name)})})}),e.jsxs(T,{title:"Recommendation setup",intro:"The prompt appears the next time this person opens Memby on any of their televisions.",icon:"sparkle",tone:"note",footer:se.completed?e.jsx($,{busy:i==="reset-rec",onClick:()=>b({kind:"reset-recommendations"}),children:"Clear stored choices"}):se.prompted?e.jsx($,{onClick:()=>b({kind:"cancel-prompt"}),children:"Cancel prompt"}):e.jsx($,{variant:"primary",busy:i==="prompt",onClick:()=>void E("prompt",()=>F.put(`${o}/recommendations/prompt`),"Setup prompt queued."),children:"Send setup prompt"}),children:[e.jsx("div",{className:"row tight",children:se.completed?e.jsx(M,{tone:"ok",children:"completed"}):se.prompted?e.jsx(M,{tone:"warn",children:"prompt queued"}):e.jsx(M,{children:"not invited"})}),e.jsx(Bn,{prompt:se})]})]}),(ue=y.watchTime)!=null&&ue.matched?e.jsx(T,{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 household's own time.",icon:"pulse",tone:"data",actions:y.watchTime.tracearrUsername?e.jsx(ie,{children:y.watchTime.tracearrUsername}):null,children:e.jsx(le,{tiles:[{label:`this week · ${w(y.watchTime.weekSessions)} session${y.watchTime.weekSessions===1?"":"s"}`,value:ke(y.watchTime.weekMs),icon:"pulse",tone:"data"},{label:`this month · ${w(y.watchTime.monthSessions)} session${y.watchTime.monthSessions===1?"":"s"}`,value:ke(y.watchTime.monthMs),icon:"calendar",tone:"info"},{label:"since Tracearr started recording",value:ke(y.watchTime.totalMs),icon:"clock",tone:"note"},{label:"last watched",value:P(y.watchTime.lastWatchedAt),icon:"history",tone:void 0,small:!0}]})}):null,e.jsx(T,{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(M,{tone:"ok",children:"enabled"}):e.jsx(M,{children:"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:i==="notifications",onClick:()=>void E("notifications",()=>F.put(`${o}/notifications`,f),"Notification settings saved.",()=>h(null)),children:"Save notifications"}),e.jsx($,{onClick:()=>{h(null),l()},children:"Discard changes"})]}),children:f?e.jsxs("div",{className:"checks columns",children:[e.jsx(z,{label:"All notifications",hint:"The master switch. Turning this off hides every optional notification below.",checked:f.enabled,onChange:A=>h(L=>L&&{...L,enabled:A})}),e.jsx(z,{label:"My Shows return dates",hint:"Remind this person when a followed show is about to return.",checked:f.showReturnAlerts,disabled:!f.enabled,onChange:A=>h(L=>L&&{...L,showReturnAlerts:A})}),e.jsx(z,{label:"Sonarr television alerts",hint:"New episodes, additions and cancellation news supplied by Sonarr.",checked:f.sonarrAlerts,disabled:!f.enabled,onChange:A=>h(L=>L&&{...L,sonarrAlerts:A})}),e.jsx(z,{label:"Radarr film alerts",hint:"Notify this person when Radarr imports a new film.",checked:f.radarrAlerts,disabled:!f.enabled,onChange:A=>h(L=>L&&{...L,radarrAlerts:A})}),e.jsx(z,{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:A=>h(L=>L&&{...L,updateAlerts:A})}),e.jsx(z,{label:"Library activity",hint:"Show alerts after the Memby library catalogue is refreshed.",checked:f.libraryAlerts,disabled:!f.enabled,onChange:A=>h(L=>L&&{...L,libraryAlerts:A})}),e.jsx(z,{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:A=>h(L=>L&&{...L,watchTimeDigest:A})}),e.jsx(z,{label:"Service status",hint:"Memby deployment and Emby outage or recovery notices. Maintenance mode itself still applies.",checked:f.systemAlerts,disabled:!f.enabled,onChange:A=>h(L=>L&&{...L,systemAlerts:A})})]}):null}),e.jsx(T,{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:ee.saved?e.jsxs(M,{tone:ee.source==="admin"?"warn":"ok",children:["r",w(ee.revision)," · ",ee.source||"device"," · ",P(ee.updatedAt)]}):e.jsx(M,{children:"defaults · never synced"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:i==="push",onClick:()=>void E("push",()=>F.put(`${o}/preferences`,{preferences:v??{}}),"Pushed to their televisions.",()=>g(null)),children:"Push to their televisions"}),e.jsx($,{onClick:()=>{g(null),l()},children:"Discard changes"}),e.jsx($,{onClick:()=>b({kind:"reset-preferences"}),children:"Restore defaults"}),e.jsx(re,{className:"crumb",to:`/admin/accounts/${encodeURIComponent(s)}/settings`,children:"History and rollback →"})]}),children:H.map(A=>e.jsxs("div",{className:"group",children:[e.jsx("p",{className:"group-label",children:A.name}),A.definitions.map(L=>e.jsx(Wn,{definition:L,value:v==null?void 0:v[L.key],onChange:N=>g(D=>({...D??{},[L.key]:N}))},L.key))]},A.name))}),e.jsx(T,{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:(y.themes??[]).length===0?e.jsx(M,{children:"all schemes"}):e.jsxs(M,{tone:"note",children:[w((y.themes??[]).length)," of ",w(S.length)]}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:i==="themes",onClick:()=>(u??[]).length===0?b({kind:"no-themes"}):void E("themes",()=>F.put(`${o}/themes`,{themes:u??[]}),"Colour schemes saved.",()=>m(null)),children:"Save colour schemes"}),e.jsx($,{onClick:()=>m(S.map(A=>A.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:S.map(A=>{var L,N,D;return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:(u??[]).includes(A.id),onChange:_=>m(te=>_.target.checked?[...te??[],A.id]:(te??[]).filter(pe=>pe!==A.id))}),e.jsx("span",{className:"switch"}),e.jsx("span",{className:"swatch",style:{"--swatch-surface":Je((L=A.palette)==null?void 0:L.surface),"--swatch-accent":Je((N=A.palette)==null?void 0:N.accent),"--swatch-hairline":Je((D=A.palette)==null?void 0:D.hairline)},children:e.jsx("i",{})}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:A.name}),e.jsx("p",{children:A.description})]})]},A.id)})})}),e.jsx(T,{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:()=>b({kind:"remove-account"}),children:"Remove Memby access"})}),k?e.jsx(Hn,{initial:k.name,busy:i==="rename",onCancel:()=>x(null),onConfirm:A=>void E("rename",()=>F.put(`${o}/devices/${encodeURIComponent(k.id)}`,{deviceName:A}),"Device renamed.",()=>x(null))}):null,j?e.jsx(zn,{pending:j,busy:i,username:y.username,onCancel:()=>b(null),onConfirm:()=>{switch(j.kind){case"remove-device":return void E("remove-device",()=>F.del(`${o}/devices/${encodeURIComponent(j.deviceId)}`),"Device signed out.");case"remove-account":return void E("remove-account",()=>F.del(`${o}/sessions`),"Memby access removed.",()=>n("/admin/accounts"));case"reset-recommendations":case"cancel-prompt":return void E("reset-rec",()=>F.del(`${o}/recommendations`),"Recommendation choices cleared.");case"reset-preferences":return void E("reset-prefs",()=>F.del(`${o}/preferences`),"Defaults restored.",()=>g(null));case"no-themes":return void E("themes",()=>F.put(`${o}/themes`,{themes:[]}),"Colour schemes saved.",()=>m(null))}}}):null]})}function Bn({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(r=>e.jsxs(ie,{tone:"warn",children:[r.title," · ",w(r.rating)," ★"]},`r:${r.title}`)),...t.flatMap(([r,o])=>(o??[]).map(a=>e.jsxs(ie,{children:[r,": ",a]},`${r}:${a}`)))];return i.length===0?e.jsx(Y,{children:"No recommendation selections have been saved."}):e.jsx("div",{className:"chips",children:i})}function Wn({definition:s,value:n,onChange:t}){if(s.kind==="toggle")return e.jsx(z,{label:s.name,hint:s.description,checked:!!n,onChange:t});if(s.kind==="choice"||s.kind==="number"){const r=s.unit??"",o=s.kind==="number"?(s.numbers??[]).map(a=>({value:String(a),label:a===0?"No limit":r?`${a} ${r}`:String(a)})):s.options??[];return e.jsx(q,{label:s.name,hint:s.description,children:e.jsx("select",{value:String(n??""),onChange:a=>t(s.kind==="number"?Number(a.target.value):a.target.value),children:o.map(a=>e.jsx("option",{value:a.value,children:a.label},a.value))})})}if(s.kind==="multi"){const r=Array.isArray(n)?n:[],o=[...r,...(s.options??[]).map(a=>a.value).filter(a=>!r.includes(a))];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:o.map(a=>{const d=(s.options??[]).find(c=>c.value===a);return d?e.jsx(z,{label:d.label,checked:r.includes(a),onChange:c=>t(c?[...r,a]:r.filter(l=>l!==a))},a):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:"Generated from their name",onChange:r=>t(r.target.value.toLocaleUpperCase("en-NZ"))})});const i=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:i.join(` +`),onChange:r=>t(r.target.value.split(` +`).map(o=>o.trim()).filter(Boolean))})})}function Hn({initial:s,busy:n,onConfirm:t,onCancel:i}){const[r,o]=p.useState(s||"Memby TV");return e.jsx("div",{className:"scrim",onPointerDown:a=>a.target===a.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(q,{label:"Device name",children:e.jsx("input",{type:"text",value:r,autoFocus:!0,maxLength:80,onChange:a=>o(a.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:i,children:"Cancel"}),e.jsx($,{variant:"primary",busy:n,disabled:!r.trim(),onClick:()=>t(r.trim()),children:"Rename"})]})]})})}function zn({pending:s,busy:n,username:t,onConfirm:i,onCancel:r}){const a={"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},"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(xe,{title:a.title,body:a.body,confirmLabel:a.label,destructive:a.destructive,busy:!!n,onConfirm:i,onCancel:r})}function Kn(s,n){if(s.kind==="toggle")return n?"On":"Off";if(s.kind==="choice"){const i=(s.options??[]).find(r=>r.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 r;return((r=(s.options??[]).find(o=>o.value===i))==null?void 0:r.label)??i}).join(", ")}function Gn(){const{userId:s=""}=We(),{wrap:n}=ne(),{busy:t,run:i}=X(),r=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:o,error:a,loading:d,reload:c}=Q(`${r}/preferences/history`,{pollMs:3e4}),[l,v]=p.useState(new Set),[g,u]=p.useState(null),m=(o==null?void 0:o.username)||"this account",f=(o==null?void 0:o.devices)??[],h=(o==null?void 0:o.revisions)??[],j=(o==null?void 0:o.catalogue)??[],b=x=>v(y=>{const R=new Set(y);return R.has(x)?R.delete(x):R.add(x),R}),k=x=>i("restore",async()=>{await n(()=>F.post(`${r}/preferences/revisions/${x}/restore`),`Restored r${x}.`),u(null),await c()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Settings history",intro:`Every change to ${m}'s synced settings, and which of their televisions has taken it.`,crumbs:e.jsxs(re,{to:`/admin/accounts/${encodeURIComponent(s)}`,children:["← ",m]})}),e.jsx(V,{message:a}),d?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(T,{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:o!=null&&o.saved?e.jsxs(M,{tone:o.currentSource==="admin"?"warn":"ok",children:["now on r",w(o.currentRevision)," · ",o.currentSource||"device"]}):e.jsx(M,{children:"defaults · never synced"}),children:f.length===0?e.jsx(Y,{children:"No television has been signed in to this account."}):e.jsx("div",{className:"list",children:f.map(x=>{const y=x.never?void 0:x.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":y})," ",x.name||"Memby TV"," ",x.signedOut?e.jsx(M,{children:"signed out"}):null]}),e.jsxs("p",{children:[x.never?"Has not fetched these settings yet":`Holding r${w(x.revision)} · taken ${P(x.ackedAt)}`,x.clientVersion?` · Memby ${x.clientVersion}`:"",x.signedOut?"":` · last seen ${P(x.lastSeen)}`]})]}),e.jsx("div",{className:"list-actions",children:x.never?e.jsx(M,{children:"never taken one"}):x.behind?e.jsxs(M,{tone:"bad",children:[w(x.behind)," behind"]}):e.jsx(M,{tone:"ok",children:"up to date"})})]},x.deviceId||x.name)})})}),e.jsx(T,{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(Z,{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(ae,{columns:6,children:"Nothing has been changed on this account yet."}):h.flatMap(x=>{const y=l.has(x.revision),R=x.acks??[],S=x.changes??[],H=[e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(x.createdAt)}),e.jsxs("td",{className:"num nowrap",children:["r",w(x.revision)," ",x.current?e.jsx(M,{tone:"ok",children:"current"}):null]}),e.jsxs("td",{className:"nowrap",children:[e.jsx(M,{tone:x.source==="admin"?"warn":"ok",children:x.author}),x.restoredFrom?e.jsxs("span",{className:"muted",children:[" restored r",w(x.restoredFrom)]}):null]}),e.jsx("td",{className:"muted",children:x.initial?e.jsx("span",{className:"muted",children:"First recorded settings"}):S.length===0?e.jsx("span",{className:"muted",children:"No visible change"}):e.jsx("div",{className:"chips",children:S.map((E,I)=>e.jsxs(ie,{children:[E.name,": ",E.before," → ",E.after]},`${E.name}:${I}`))})}),e.jsx("td",{className:"num",children:R.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{title:R.map(E=>E.deviceName||E.deviceId).join(", "),children:w(R.length)})}),e.jsx("td",{className:"num nowrap",children:e.jsxs("span",{className:"list-actions",children:[e.jsx($,{size:"sm",onClick:()=>b(x.revision),children:y?"Hide":"Show"}),x.current?null:e.jsx($,{size:"sm",onClick:()=>u(x.revision),children:"Restore"})]})})]},x.revision)];return y&&H.push(e.jsx("tr",{children:e.jsx("td",{colSpan:6,className:"muted",children:e.jsx("div",{className:"chips",children:j.map(E=>{var I;return e.jsxs(ie,{children:[E.name,":"," ",Kn(E,(I=x.preferences)==null?void 0:I[E.key])]},E.key)})})})},`${x.revision}:detail`)),H})})]})})})]}),g!==null?e.jsx(xe,{title:`Restore revision ${g}?`,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 k(g),onCancel:()=>u(null)}):null]})}function Zn({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(ie,{tone:t.version===s.version?"ok":void 0,children:t.version},t.version))})}function Jn(){const{status:s,error:n,loading:t}=oe(),i=(s==null?void 0:s.clients)??[],r=i.filter(a=>(a.capabilities??[]).includes("server_features_v1")),o=new Set(i.map(a=>a.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(V,{message:n}),t?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"devices known",value:w(i.length),icon:"tv",tone:"info"},{label:"active in the last quarter hour",value:w(i.filter(a=>$e(a.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"reporting their capabilities",value:w(r.length),icon:"sliders",tone:"ok"},{label:"app builds in service",value:w(o.size),icon:"download",tone:"note"}]}),e.jsx(T,{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(Z,{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(ae,{columns:6,children:"No devices have signed in yet."}):i.map(a=>{const d=(a.capabilities??[]).includes("server_features_v1"),c=rs(a.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(re,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(a.deviceId)}`,children:a.deviceName||"Memby TV"})]})}),e.jsx("td",{className:"muted",children:a.username}),e.jsx("td",{className:"mono",children:a.version||"legacy"}),e.jsx("td",{children:e.jsx(Zn,{client:a})}),e.jsx("td",{children:e.jsx(M,{tone:d?"ok":"warn",children:d?"reported":"missing"})}),e.jsx("td",{className:"nowrap muted",children:P(a.lastSeen)})]},`${a.deviceId}:${a.username}`)})})]})})})]})]})}const bs={user:"",q:"",ip:"",outcome:"",from:"",to:""},Yn=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Qn(){var x,y,R,S,H,E;const[s,n]=p.useState("log"),[t,i]=p.useState(7),[r,o]=p.useState(bs),[a,d]=p.useState(0),c=100,l=p.useMemo(()=>Te({...r,days:r.from?void 0:t||void 0,limit:c,offset:a*c}),[r,t,a]),v=Q(`/admin/api/logins${l}`,{enabled:s==="log"}),g=Q(`/admin/api/logins/devices${l}`,{enabled:s==="devices"}),u=((x=v.data)==null?void 0:x.users)??((y=g.data)==null?void 0:y.users)??[],m=((R=v.data)==null?void 0:R.totals)??((S=g.data)==null?void 0:S.totals),f=((H=v.data)==null?void 0:H.retentionDays)??((E=g.data)==null?void 0:E.retentionDays)??90,h=s==="log"?v.loading:g.loading,j=s==="log"?v.error:g.error,b=I=>{o(G=>({...G,...I})),d(0)},k=Object.entries(r).some(([,I])=>I!=="")||!!r.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(Be,{value:s,options:[{value:"log",label:"Log"},{value:"devices",label:"By device"}],onChange:n})}),e.jsx(V,{message:j}),m?e.jsx(le,{tiles:[{label:"Successful sign-ins",value:w(m.logins),icon:"key",tone:"ok"},{label:"Refused",value:w(m.failures),icon:"shield",tone:m.failures>0?"warn":void 0},{label:"Televisions",value:w(m.devices),icon:"tv",tone:"info"},{label:"People",value:w(m.users),icon:"people",tone:"note"},{label:"Addresses",value:w(m.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(Be,{value:r.from?-1:t,options:Yn.map(I=>({value:I.value,label:I.label})),onChange:I=>{i(I),b({from:"",to:""})}})}),e.jsx(q,{label:"Person",children:e.jsxs("select",{value:r.user,onChange:I=>b({user:I.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),u.map(I=>e.jsx("option",{value:I.id,children:I.username||I.id},I.id))]})}),e.jsx(q,{label:"Outcome",children:e.jsxs("select",{value:r.outcome,onChange:I=>b({outcome:I.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:r.ip,placeholder:"10.0.0.4",onChange:I=>b({ip:I.target.value})})}),e.jsx(q,{label:"From",children:e.jsx("input",{type:"date",value:r.from,onChange:I=>b({from:I.target.value})})}),e.jsx(q,{label:"To",children:e.jsx("input",{type:"date",value:r.to,onChange:I=>b({to:I.target.value})})}),e.jsx(q,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:r.q,placeholder:"Name, device or address",onChange:I=>b({q:I.target.value})})}),e.jsx("div",{className:"filter-actions",children:k?e.jsx($,{variant:"quiet",size:"sm",onClick:()=>{o(bs),d(0)},children:"Clear"}):null})]}),h?e.jsx(W,{}):s==="log"?e.jsx(Xn,{data:v.data,page:a,limit:c,onPage:d}):e.jsx(et,{data:g.data})]})}function Xn({data:s,page:n,limit:t,onPage:i}){if(!s)return null;const r=s.events.length,o=s.total===0?0:n*t+1;return e.jsxs(e.Fragment,{children:[e.jsxs(he,{cols:"wide",children:[e.jsx(T,{title:"Attempts per day",intro:"Grouped in the household's own timezone, so an evening sign-in stays on the day it happened.",icon:"chart",tone:"info",children:e.jsx(Qs,{data:s.days,labelOf:a=>a.day,valueOf:a=>a.logins+a.failures,toneOf:a=>a.failures>a.logins?"bad":void 0,title:a=>`${a.day}: ${a.logins} in, ${a.failures} refused, ${a.devices} televisions`})}),e.jsx(T,{title:"Where from",icon:"globe",tone:"data",children:s.addresses.length===0?e.jsx(Y,{children:"No addresses in this window."}):e.jsx("div",{className:"list",children:s.addresses.slice(0,8).map(a=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:a.ipAddress}),e.jsxs("p",{children:[w(a.logins)," in",a.failures>0?` · ${w(a.failures)} refused`:""]})]}),a.failures>0&&a.logins===0?e.jsx(M,{tone:"bad",children:"only refused"}):null]},a.ipAddress))})})]}),e.jsx(T,{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(o)}–${w(o+r-1)} of ${w(s.total)}`}),footer:s.total>t?e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",disabled:n===0,onClick:()=>i(n-1),children:"Newer"}),e.jsx($,{size:"sm",disabled:(n+1)*t>=s.total,onClick:()=>i(n+1),children:"Older"})]}):void 0,children:e.jsx(Z,{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(ae,{columns:6,children:"No sign-in attempts match these filters."}):s.events.map(a=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(a.occurredAt)}),e.jsx("td",{children:a.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{children:a.deviceId?e.jsx(re,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(a.deviceId)}`,children:a.deviceName||a.deviceId}):e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"mono nowrap",children:a.ipAddress||"—"}),e.jsx("td",{className:"mono",children:a.clientVersion||"—"}),e.jsx("td",{className:"nowrap",children:a.success?a.newDevice?e.jsx(M,{tone:"info",children:"first sign-in"}):e.jsx(M,{tone:"ok",children:"got in"}):e.jsx(M,{tone:"bad",children:a.failureReason||"refused"})})]},a.id))})]})})})]})}function et({data:s}){return s?e.jsx(T,{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(Z,{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(ae,{columns:9,children:"No television has connected in this window."}):s.devices.map(n=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(re,{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?P(n.lastLogin):"—"}),e.jsx("td",{className:"mono",children:n.clientVersion||"—"})]},n.deviceId))})]})})}):null}const st=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function nt(){const{deviceId:s=""}=We(),[n,t]=p.useState(7),i=p.useMemo(()=>`/admin/api/logins/devices/${encodeURIComponent(s)}${Te({days:n||void 0,limit:200})}`,[s,n]),{data:r,error:o,loading:a}=Q(i,{enabled:!!s}),d=r==null?void 0:r.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(re,{to:"/admin/clients",children:"Devices"}),e.jsx("span",{children:"/"}),e.jsx(re,{to:"/admin/logins",children:"Sign-ins"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:c})]}),actions:e.jsx(Be,{value:n,options:st.map(l=>({value:l.value,label:l.label})),onChange:t})}),e.jsx(V,{message:o}),a?e.jsx(W,{}):r?e.jsxs(e.Fragment,{children:[e.jsx(le,{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?P(d.firstLogin):"—",small:!0,icon:"history"},{label:"Last seen",value:d!=null&&d.lastLogin?P(d.lastLogin):"—",small:!0,icon:"pulse",tone:"note"}]}),e.jsxs(he,{cols:"wide",children:[e.jsx(T,{title:"Connections per day",icon:"chart",tone:"info",children:e.jsx(Qs,{data:r.days,labelOf:l=>l.day,valueOf:l=>l.logins+l.failures,toneOf:l=>l.failures>l.logins?"bad":void 0,title:l=>`${l.day}: ${l.logins} in${l.failures?`, ${l.failures} refused`:""}`})}),e.jsxs("div",{className:"stack",children:[e.jsx(T,{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:r.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(T,{title:"Builds",intro:"Kept per television rather than per session, so it survives a sign-out.",icon:"upload",tone:"note",children:r.versions.length===0?e.jsx(Y,{children:"No build history for this television."}):e.jsx("div",{className:"list",children:r.versions.map(l=>e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:l.version}),e.jsxs("p",{children:[P(l.firstSeen)," → ",P(l.lastSeen)]})]})},l.version))})})]})]}),e.jsx(T,{title:"Addresses",icon:"globe",tone:"data",children:r.addresses.length===0?e.jsx(Y,{children:"No addresses recorded in this window."}):e.jsx("div",{className:"chips",children:r.addresses.map(l=>e.jsxs(ie,{tone:l.failures>0?"warn":"data",children:[l.ipAddress," · ",w(l.logins),l.failures>0?` (+${w(l.failures)} refused)`:""]},l.ipAddress))})}),e.jsx(T,{title:"Every attempt",icon:"key",tone:"ok",actions:e.jsxs("span",{className:"filter-summary",children:[w(r.total)," in this window"]}),children:e.jsx(Z,{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:r.events.length===0?e.jsx(ae,{columns:6,children:"This television has not connected in the selected window."}):r.events.map(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(l.occurredAt)}),e.jsx("td",{children:l.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{className:"mono nowrap",children:l.ipAddress||"—"}),e.jsx("td",{className:"mono",children:l.clientVersion||"—"}),e.jsx("td",{className:"muted",children:l.method}),e.jsx("td",{className:"nowrap",children:l.success?l.newDevice?e.jsx(M,{tone:"info",children:"first sign-in"}):e.jsx(M,{tone:"ok",children:"got in"}):e.jsx(M,{tone:"bad",children:l.failureReason||"refused"})})]},l.id))})]})})})]}):null]})}function tt(){const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(!1),l=(s==null?void 0:s.library.byType)??{},v=!!(s!=null&&s.syncRunning),g=u=>a(u,async()=>{await r(()=>F.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(V,{message:n}),t||!s?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"items",value:w(s.library.total),icon:"library",tone:"data"},...Object.keys(l).sort().map(u=>({label:u,value:w(l[u]),icon:"list"})),{label:"last import",value:P(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsx(T,{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:v?"Import running…":`An incremental import runs automatically every ${s.syncEvery}.`}),children:e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",icon:"sync",disabled:v,busy:o==="incremental",onClick:()=>void g("incremental"),children:"Sync new items"}),e.jsx($,{icon:"database",disabled:v,onClick:()=>c(!0),children:"Full re-import"})]})})]}),d?e.jsx(xe,{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:o==="full",onConfirm:()=>void g("full"),onCancel:()=>c(!1)}):null]})}const it={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 at(){const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(!1),[l,v]=p.useState(""),[g,u]=p.useState(!1),[m,f]=p.useState([]),[h,j]=p.useState(!1),b=s==null?void 0:s.mdblist;p.useEffect(()=>{h||!b||(c(b.enabled),f(b.sources??[]))},[b,h]);const k=()=>a("save",async()=>{await r(()=>F.post("/admin/api/mdblist-settings",{enabled:d,apiKey:l.trim(),clearApiKey:g,sources:m}),"Ratings settings saved."),v(""),u(!1),j(!1),await i()}),x=(b==null?void 0:b.cachedTitles)??0;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Movie ratings",intro:"Optional MDBList scores on films and shows."}),e.jsx(V,{message:n}),t||!b?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"titles stored",value:w(x),icon:"database",tone:"data"},{label:"due to be re-checked",value:w(b.staleTitles),icon:"sync",tone:"warn"},{label:"sources shown",value:w((b.sources??[]).length),icon:"star",tone:"note"},{label:"API key",value:b.apiKeyConfigured?"saved":"not set",small:!0,icon:"key",tone:b.apiKeyConfigured?"ok":void 0}]}),e.jsxs(he,{cols:"2",children:[e.jsxs(T,{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(M,{tone:"ok",children:["on · ",m.length," sources"]}):e.jsx(M,{children:b.apiKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(z,{label:"Show external ratings on televisions",hint:"Off leaves the stored ratings in place.",checked:d,onChange:y=>{c(y),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:l,placeholder:b.apiKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:y=>v(y.target.value)})}),e.jsx(z,{label:"Remove the saved key",checked:g,onChange:u})]}),e.jsx(T,{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:(b.availableSources??[]).length===0?e.jsx(Y,{children:"No rating sources are available."}):e.jsx("div",{className:"checks columns",children:(b.availableSources??[]).map(y=>e.jsx(z,{label:it[y]??y,checked:m.includes(y),onChange:R=>{j(!0),f(S=>R?[...S,y]:S.filter(H=>H!==y))}},y))})})]}),e.jsx(T,{children:e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",busy:o==="save",onClick:()=>void k(),children:"Save ratings settings"}),e.jsx("span",{className:"hint",children:x?"Ratings are fetched as televisions browse, never on the request path.":"No ratings stored yet. They are saved as televisions browse the library."})]})})]})]})}function rt(){var g;const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),d=(s==null?void 0:s.requestUsers)??[],c=((g=s==null?void 0:s.requestPolicy)==null?void 0:g.allowedUserIds)??[],l=p.useMemo(()=>new Map(((s==null?void 0:s.requestUsage)??[]).map(u=>[u.userId,u])),[s==null?void 0:s.requestUsage]),v=u=>a(`access-${u}`,async()=>{const m=c.includes(u)?c.filter(f=>f!==u):[...c,u];await r(()=>F.post("/admin/api/request-policy",{allowedUserIds:m}),m.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(V,{message:n}),t?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(T,{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(M,{tone:s!=null&&s.radarrReady?"ok":"bad",children:["Movies ",s!=null&&s.radarrReady?"ready":"not configured"]}),e.jsxs(M,{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(Y,{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(T,{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(Y,{children:"No one has signed in yet."}):e.jsx(Z,{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 m=l.get(u.id),f=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:P(u.lastSeen)}),e.jsx("td",{className:"num",children:(m==null?void 0:m.requests)??0}),e.jsx("td",{className:"muted nowrap",children:m!=null&&m.lastRequest?P(m.lastRequest):"—"}),e.jsx("td",{children:e.jsx($,{size:"sm",variant:f?"quiet":"primary",busy:o===`access-${u.id}`,onClick:()=>void v(u.id),children:f?"Remove access":"Give access"})})]},u.id)})})]})})})]})]})}function lt(){const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(!1),l=s==null?void 0:s.forYou,v=!!(s!=null&&s.forYouRunning),g=(u,m,f)=>a(m,async()=>{await r(()=>F.post("/admin/api/for-you",{action:u}),f),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(V,{message:n}),t?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"Tracearr sessions",value:w((l==null?void 0:l.tracearrSessions)??0),icon:"play",tone:"info"},{label:"user profiles",value:w((l==null?void 0:l.profiles)??0),icon:"people",tone:"note"},{label:"ranked candidates",value:w((l==null?void 0:l.candidates)??0),icon:"sparkle",tone:"note"},{label:"last full import",value:P(l==null?void 0:l.lastFullImport),small:!0,icon:"clock"}]}),e.jsx(T,{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:v?"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:v,busy:o==="import",onClick:()=>void g("incremental-import","import","Import started."),children:"Import recent sessions"}),e.jsx($,{icon:"database",disabled:v,onClick:()=>c(!0),children:"Full Tracearr backfill"}),e.jsx($,{icon:"sync",disabled:v,busy:o==="rebuild",onClick:()=>void g("rebuild-all","rebuild","Rebuild started."),children:"Rebuild all pools"})]})}),e.jsx(T,{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(re,{to:"/admin/inspector",children:"Open the inspector"}),children:e.jsx(e.Fragment,{})})]}),d?e.jsx(xe,{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:o==="full",onConfirm:()=>void g("full-import","full","Backfill started."),onCancel:()=>c(!1)}):null]})}const ot=[["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 ct(s){return s?ot.flatMap(([n,t])=>Object.entries(s[t]??{}).map(([i,r])=>({dimension:n,name:i,weight:r.weight??0,evidence:r.evidence??0}))).sort((n,t)=>Math.abs(t.weight)-Math.abs(n.weight)):[]}const fs=s=>`${s>=0?"+":""}${s.toFixed(3)}`;function dt(){const{status:s}=oe(),{wrap:n}=ne(),{busy:t,run:i}=X(),[r,o]=p.useState(""),[a,d]=p.useState("default"),[c,l]=p.useState("0"),[v,g]=p.useState(""),[u,m]=p.useState(null),[f,h]=p.useState("Choose a person to inspect their recommendations."),[j,b]=p.useState(""),k=(s==null?void 0:s.requestUsers)??[],x=()=>i("run",async()=>{if(!r){b("Choose a person to pressure-test.");return}b(""),h("Running the permission check and the scorer…");const E=new URLSearchParams({userId:r,context:a,minutes:c||"0",limit:"100"});v&&E.set("at",new Date(v).toISOString());const I=await n(()=>F.get(`/admin/api/recommendations?${E.toString()}`));I?(m(I),h(`Scored at ${new Date().toLocaleTimeString()}.`)):h("Pressure test failed.")}),y=ct((u==null?void 0:u.profile)??null).slice(0,24),R=(u==null?void 0:u.actions)??[],S=(u==null?void 0:u.items)??[],H=(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(V,{message:j}),e.jsx(T,{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 x(),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:r,onChange:E=>o(E.target.value),children:[e.jsx("option",{value:"",children:"Choose a person…"}),k.map(E=>e.jsx("option",{value:E.id,children:E.username},E.id))]})}),e.jsx(q,{label:"Context",children:e.jsxs("select",{value:a,onChange:E=>d(E.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:E=>l(E.target.value)})}),e.jsx(q,{label:"Evaluate at",children:e.jsx("input",{type:"datetime-local",value:v,onChange:E=>g(E.target.value)})})]})}),u?e.jsxs(e.Fragment,{children:[e.jsx(le,{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(S.length),icon:"sparkle",tone:"note"},{label:"source events",value:w(H.sourceEvents??0),icon:"pulse",tone:"info"},{label:"algorithm",value:H.algorithmVersion||"—",small:!0,icon:"chip"},{label:"pool built",value:P(H.poolBuiltAt),small:!0,icon:"clock"}]}),e.jsxs(T,{title:"Profile evidence",intro:"The strongest learned affinities, and every explicit action this person has taken.",icon:"sparkle",tone:"note",children:[y.length===0?e.jsx(Y,{children:"No repeated affinity evidence yet; cold-start priors apply."}):e.jsx("div",{className:"chips",children:y.map(E=>e.jsxs(ie,{tone:E.weight<0?"bad":void 0,children:[E.dimension,": ",E.name," ",fs(E.weight)," · n=",w(E.evidence)]},`${E.dimension}:${E.name}`))}),R.length===0?e.jsx(Y,{children:"No explicit recommendation actions."}):e.jsx("div",{className:"chips",children:R.map((E,I)=>e.jsxs(ie,{tone:"ok",children:[E.action,": ",E.title||E.itemId]},`${E.action}:${I}`))})]}),S.length===0?e.jsx(T,{children:e.jsx(Y,{children:"No candidates survived this context, the explicit exclusions and the permission filter."})}):e.jsx(he,{children:S.map((E,I)=>{const G=E.explanation??{},ee=Object.entries(G.components??{}).sort((A,L)=>Math.abs(L[1])-Math.abs(A[1])),se=E.exposure??{},ue=[E.type,E.year,E.runtimeMinutes?`${E.runtimeMinutes} min`:null,...E.genres??[]].filter(Boolean).join(" · ");return e.jsxs(T,{title:`#${I+1} · ${E.title}`,intro:ue,actions:e.jsx(ie,{tone:"ok",children:Number(G.total??0).toFixed(3)}),children:[e.jsxs("p",{className:"hint",children:[E.preparedReason||"No legacy prepared explanation",E.compatibilityLabel?` · ${E.compatibilityLabel}`:""]}),e.jsxs("div",{className:"chips",children:[(G.reasonCodes??[]).map(A=>e.jsx(ie,{tone:"ok",children:A},A)),ee.map(([A,L])=>e.jsxs(ie,{tone:L<0?"bad":void 0,children:[A,"=",fs(L)]},A))]}),e.jsxs("details",{children:[e.jsx("summary",{className:"muted",children:"Pool, row and exposure detail"}),e.jsxs("p",{className:"hint",children:["Base rank ",w(E.baseRank??0)," · base ",Number(E.baseScore??0).toFixed(3)," · affinity ",Number(E.affinityScore??0).toFixed(3)," · compatibility"," ",Number(E.compatibilityScore??0).toFixed(3)," · impressions"," ",w(se.impressions??0)," · focuses ",w(se.focuses??0)," · selects"," ",w(se.selects??0)]}),e.jsx("div",{className:"chips",children:(E.eligibleRows??[]).map(A=>e.jsx(ie,{tone:"ok",children:A},A))}),E.preparedEvidenceTitle?e.jsxs("p",{className:"hint",children:["Prepared evidence: ",E.preparedEvidenceTitle]}):null]})]},`${I}:${E.title}`)})})]}):null]})}const ht=4,we=[{id:"home",label:"Home",type:"films and television shows"},{id:"movies",label:"Movies",type:"films"},{id:"tv_shows",label:"TV Shows",type:"television shows"}],Ee=()=>({pinnedItems:[],primeSubtitle:""}),ns=[{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 ys(s){const n=s.getTimezoneOffset()*6e4;return new Date(s.getTime()-n).toISOString().slice(0,16)}function ws(s){return!!(s&&Number.isFinite(new Date(s).getTime())&&new Date(s).getFullYear()>=2e3)}function ut(s){return s.frequency??"once"}function mt(s){if(s.frequency==="daily")return`Every day · ${s.startTime}–${s.endTime}`;if(s.frequency==="weekly"){const n=ns.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 ks(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),r=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:[r?n:"home"]}}function pt(){var ue,A,L;const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r,show:o}=ne(),{busy:a,run:d}=X(),[c,l]=p.useState("home"),[v,g]=p.useState({home:Ee(),movies:Ee(),tv_shows:Ee()}),[u,m]=p.useState(!1),[f,h]=p.useState(""),[j,b]=p.useState(null),[k,x]=p.useState([]),[y,R]=p.useState(null),S=s==null?void 0:s.heroPolicy;p.useEffect(()=>{var N,D,_;u||!S||(g({home:((N=S.placements)==null?void 0:N.home)??{pinnedItems:S.pinnedItems??[],primeSubtitle:S.primeSubtitle??""},movies:((D=S.placements)==null?void 0:D.movies)??Ee(),tv_shows:((_=S.placements)==null?void 0:_.tv_shows)??Ee()}),x(S.schedules??[]))},[S,u]);const H=()=>d("search",async()=>{const N=f.trim();if(!N)return;const D=await r(()=>F.get(`/admin/api/hero/search?q=${encodeURIComponent(N)}`));D&&b(D.items??[])}),E=v[c],I=E.pinnedItems??[],G=N=>{g(D=>({...D,[c]:{...D[c],...N}})),m(!0)},ee=N=>{if(!I.some(D=>D.id===N.id)){if(I.length>=ht){o("Remove a pinned title before adding another.","bad");return}G({pinnedItems:[...I,N]})}},se=()=>d("save",async()=>{await r(()=>F.post("/admin/api/hero-policy",{placements:Object.fromEntries(Object.entries(v).map(([N,D])=>[N,{pinnedItemIds:(D.pinnedItems??[]).map(_=>_.id),primeSubtitle:D.primeSubtitle.trim()}])),schedules:k}),"Hero saved."),m(!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(V,{message:n}),t?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(he,{children:we.map(N=>{var Le;const D=v[N.id],_=k.filter(me=>me.enabled&&(me.placements??["home"]).includes(N.id)).sort((me,Ge)=>Ge.priority-me.priority)[0],te=(D.pinnedItems??[]).length?"Manual":_?"Schedule ready":"Automatic",pe=(Le=S==null?void 0:S.items)==null?void 0:Le.find(me=>me.id===(_==null?void 0:_.itemId)),Ie=(D.pinnedItems??[]).map(me=>me.name).join(", ")||(pe==null?void 0:pe.name)||(_==null?void 0:_.itemId)||"Resolved for each viewer";return e.jsx(T,{title:N.label,intro:`${te} · ${Ie}`,tone:N.id===c?"info":void 0,children:e.jsxs($,{size:"sm",variant:"quiet",onClick:()=>l(N.id),children:["Manage ",N.label]})},N.id)})}),e.jsx("div",{className:"tabs",role:"tablist","aria-label":"Hero placement",children:we.map(N=>e.jsx($,{variant:c===N.id?"primary":"quiet",onClick:()=>l(N.id),children:N.label},N.id))}),e.jsxs(T,{title:`${(ue=we.find(N=>N.id===c))==null?void 0:ue.label} hero`,intro:`Pinned ${(A=we.find(N=>N.id===c))==null?void 0:A.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:a==="save",onClick:()=>void se(),children:"Save hero"}),e.jsx($,{onClick:()=>{G({pinnedItems:[]})},children:"Clear pins"}),u?e.jsx("span",{className:"hint",children:"Unsaved changes."}):null]}),children:[I.length===0?e.jsx(Y,{children:"No titles are pinned. The hero is entirely release-aware and automatic."}):e.jsx("div",{className:"hero-pins",children:I.map((N,D)=>e.jsxs("div",{className:"hero-pin",children:[e.jsx("span",{className:"hero-pin-order",children:D+1}),e.jsxs("span",{children:[e.jsx("b",{children:N.name}),e.jsxs("small",{children:[N.type,N.year?` · ${N.year}`:""]})]}),e.jsx($,{size:"sm",icon:"clock",onClick:()=>R(ks(N,c)),children:"Schedule"}),e.jsx($,{variant:"quiet",size:"sm",icon:"close",title:`Remove ${N.name}`,onClick:()=>G({pinnedItems:I.filter(_=>_.id!==N.id)})})]},N.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:E.primeSubtitle,placeholder:"Leave blank for the automatic reason",onChange:N=>{G({primeSubtitle:N.target.value})}})})]}),e.jsx(T,{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(M,{tone:"info",children:(S==null?void 0:S.timeZone)||"server local time"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:a==="save",onClick:()=>void se(),children:"Save schedule"}),e.jsx("span",{className:"hint",children:"Daily and weekly rules repeat until you switch them off."})]}),children:k.length===0?e.jsx(Y,{children:"No scheduled heroes yet. Use Schedule beside a pinned or searched title."}):e.jsx("div",{className:"hero-schedule-list",children:[...k].sort((N,D)=>Number(D.enabled)-Number(N.enabled)||D.priority-N.priority).map(N=>{const D=[...(S==null?void 0:S.items)??[],...I,...j??[]].find(_=>_.id===N.itemId);return e.jsxs("article",{className:"hero-schedule","data-enabled":N.enabled||void 0,children:[e.jsxs("div",{className:"hero-schedule-time",children:[e.jsx("b",{children:N.frequency==="weekly"?"Weekly":N.frequency==="daily"?"Daily":"Once"}),e.jsx("span",{children:N.frequency?N.startTime:N.startAt?new Date(N.startAt).toLocaleDateString():"—"})]}),e.jsxs("div",{className:"hero-schedule-main",children:[e.jsxs("div",{className:"hero-schedule-title",children:[e.jsx("h3",{children:(D==null?void 0:D.name)??N.itemId}),e.jsx(M,{tone:N.enabled?"ok":void 0,children:N.enabled?"enabled":"paused"})]}),e.jsx("p",{children:mt(N)}),e.jsxs("div",{className:"chips",children:[(N.placements??["home"]).map(_=>{var te;return e.jsx("span",{className:"chip",children:(te=we.find(pe=>pe.id===_))==null?void 0:te.label},_)}),N.priority!==0?e.jsxs("span",{className:"chip",children:["Priority ",N.priority]}):null]})]}),e.jsxs("div",{className:"hero-schedule-actions",children:[e.jsx($,{size:"sm",onClick:()=>R({...N}),children:"Edit"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{x(_=>_.map(te=>te.id===N.id?{...te,enabled:!te.enabled}:te)),m(!0)},children:N.enabled?"Pause":"Enable"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{x(_=>_.filter(te=>te.id!==N.id)),m(!0)},children:"Remove"})]})]},N.id)})})}),e.jsxs(T,{title:"Find a title",intro:`Search the imported Emby catalogue. Add a result to the selected ${(L=we.find(N=>N.id===c))==null?void 0:L.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:N=>h(N.target.value),onKeyDown:N=>{N.key==="Enter"&&H()}})}),e.jsx($,{busy:a==="search",icon:"search",onClick:()=>void H(),children:"Search"})]}),j===null?null:j.length===0?e.jsx(Y,{children:"No playable films or series matched that search."}):e.jsx(he,{children:j.map(N=>e.jsx(T,{title:N.name,intro:`${N.type||"Title"} · ${N.year||"Year unknown"}`,children:e.jsxs("div",{className:"row",children:[e.jsx($,{size:"sm",icon:"plus",disabled:I.some(D=>D.id===N.id)||c==="movies"&&N.type!=="Movie"||c==="tv_shows"&&N.type!=="Series",onClick:()=>ee(N),children:I.some(D=>D.id===N.id)?"Pinned":"Add to hero"}),e.jsx($,{size:"sm",icon:"clock",onClick:()=>R(ks(N,c)),children:"Schedule"})]})},N.id))})]})]}),y?e.jsx(xt,{schedule:y,item:[...(S==null?void 0:S.items)??[],...I,...j??[]].find(N=>N.id===y.itemId),timeZone:(S==null?void 0:S.timeZone)||"server local time",isNew:!k.some(N=>N.id===y.id),onCancel:()=>R(null),onSave:N=>{x(D=>D.some(_=>_.id===N.id)?D.map(_=>_.id===N.id?N:_):[...D,N]),R(null),m(!0)}}):null]})}function xt({schedule:s,item:n,timeZone:t,isNew:i,onSave:r,onCancel:o}){const[a,d]=p.useState({...s,weekdays:[...s.weekdays??[]]}),c=ut(a),l=h=>{const j=new Date,b=new Date(j.getTime()+2*60*60*1e3);d(k=>{var x;return h==="once"?{...k,frequency:void 0,startAt:ws(k.startAt)?k.startAt:j.toISOString(),endAt:ws(k.endAt)?k.endAt:b.toISOString()}:{...k,frequency:h,startTime:k.startTime||"18:00",endTime:k.endTime||"22:00",weekdays:h==="weekly"?(x=k.weekdays)!=null&&x.length?k.weekdays:[1,2,3,4,5]:[]}})},v=a.placements??["home"],g=h=>h==="home"||h==="movies"&&(n==null?void 0:n.type)==="Movie"||h==="tv_shows"&&(n==null?void 0:n.type)==="Series",u=!!(a.startAt&&a.endAt&&new Date(a.endAt)>new Date(a.startAt)),m=!!(a.startTime&&a.endTime&&a.startTime!==a.endTime&&(c!=="weekly"||(a.weekdays??[]).length>0)),f=c==="once"?u:m;return e.jsx("div",{className:"scrim",onPointerDown:h=>h.target===h.currentTarget&&o(),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)??a.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:()=>l(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:a.startAt?ys(new Date(a.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:a.endAt?ys(new Date(a.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:a.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:a.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:ns.map(j=>j.value)})),children:"Every day"})]})]}),e.jsx("div",{className:"schedule-day-grid",children:ns.map(h=>{const j=(a.weekdays??[]).includes(h.value);return e.jsx("button",{type:"button","aria-pressed":j,title:h.label,onClick:()=>d(b=>({...b,weekdays:j?(b.weekdays??[]).filter(k=>k!==h.value):[...b.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:we.map(h=>e.jsx(z,{label:h.label,checked:v.includes(h.id),disabled:!g(h.id),onChange:j=>d(b=>{const k=b.placements??["home"],x=j?[...k,h.id]:k.filter(y=>y!==h.id);return{...b,placements:x.length?[...new Set(x)]:k}})},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:a.priority,onChange:h=>d(j=>({...j,priority:Number(h.target.value)}))})})]}),e.jsx(z,{label:"Schedule enabled",hint:"Pause it without losing its days and times.",checked:a.enabled,onChange:h=>d(j=>({...j,enabled:h}))}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:o,children:"Cancel"}),e.jsx($,{variant:"primary",disabled:!f,onClick:()=>r(a),children:i?"Add rule":"Save rule"})]})]})})}function jt(){const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(null),l=s==null?void 0:s.features,v=(l==null?void 0:l.features)??[],g=(s==null?void 0:s.clients)??[],u=(l==null?void 0:l.revision)??0,m=(b,k,x,y)=>a(k,async()=>{await r(()=>F.post("/admin/api/features",{action:b,expectedRevision:u,overrides:y??{}}),x),c(null),await i()}),f=g.filter(b=>(b.capabilities??[]).includes("server_features_v1")).length,h=!!(l!=null&&l.safeMode),j=(b,k)=>({...Object.fromEntries(v.filter(x=>x.source==="override").map(x=>[x.key,x.enabled])),[b]:k});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(V,{message:n}),t||!l?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(T,{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:o==="safe",onClick:()=>h?void m("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(Xs,{tiles:[{label:"features active",value:`${v.filter(b=>b.enabled).length} / ${v.length}`},{label:"explicit overrides",value:w(v.filter(b=>b.source==="override").length)},{label:"televisions reporting the control plane",value:`${f} / ${g.length}`},{label:"published revision",value:`r${w(u)}`}]})}),e.jsx(he,{cols:"2",children:v.length===0?e.jsx(T,{title:"Nothing registered",icon:"sliders",children:e.jsx(Y,{children:"No server features are registered."})}):v.map(b=>e.jsxs(T,{title:b.name,intro:b.description,actions:e.jsx(M,{tone:b.enabled?"ok":void 0,children:b.enabled?"active":"off"}),footer:e.jsxs("span",{className:"hint",children:["↳ ",b.recovery]}),children:[e.jsx(z,{label:b.enabled?"On":"Off",hint:"Changing this applies the feature policy to every compatible television.",checked:b.enabled,disabled:o===b.key,onChange:k=>c({action:"feature",key:b.key,enabled:k,title:`${k?"Turn on":"Turn off"} ${b.name}?`,body:`${k?"Enable":"Disable"} this feature for every compatible television. ${b.recovery}`,label:k?"Turn on":"Turn off"})}),e.jsxs("div",{className:"chips",children:[e.jsx(ie,{children:b.key}),e.jsxs(ie,{children:["protocol ",w(b.minimumProtocol),"+"]}),e.jsx(ie,{tone:b.compatible?"ok":"warn",children:b.compatible?"server compatible":"compatibility blocked"}),e.jsx(ie,{tone:"note",children:b.area})]})]},b.key))}),e.jsx(T,{children:e.jsxs("div",{className:"row",children:[e.jsx($,{disabled:!l.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(M,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(M,{tone:"ok",children:["live · revision r",w(u)]})]})})]}),d?e.jsx(xe,{title:d.title,body:d.body,confirmLabel:d.label,destructive:d.action!=="rollback",busy:o===d.action,onConfirm:()=>void m(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 vt(){const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r,show:o}=ne(),{busy:a,run:d}=X(),[c,l]=p.useState(!0),[v,g]=p.useState("6.5"),[u,m]=p.useState(!1);p.useEffect(()=>{var h,j;u||!s||(l(((h=s.playbackPolicy)==null?void 0:h.prerollEnabled)!==!1),g(String((((j=s.playbackPolicy)==null?void 0:j.prerollDurationMs)??6500)/1e3)))},[s,u]);const f=()=>d("save",async()=>{const h=Number(v);if(!Number.isFinite(h)||h<1||h>30){o("The preroll duration must be between 1 and 30 seconds.","bad");return}await r(()=>F.post("/admin/api/playback-policy",{prerollEnabled:c,prerollDurationMs:Math.round(h*1e3)}),"Playback policy saved."),m(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Playback",intro:"Presentation policy sent with every playback launch."}),e.jsx(V,{message:n}),t?e.jsx(W,{rows:1}):e.jsxs(T,{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(M,{tone:"ok",children:["on · ",v,"s"]}):e.jsx(M,{children:"off"}),footer:e.jsx($,{variant:"primary",busy:a==="save",onClick:()=>void f(),children:"Save playback policy"}),children:[e.jsx(z,{label:"Show the preroll before a title starts",checked:c,onChange:h=>{l(h),m(!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:v,onChange:h=>{g(h.target.value),m(!0)}})})})]})]})}function gt(){const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(null),[l,v]=p.useState(null),[g,u]=p.useState(!1),m=s==null?void 0:s.subtitles,f=m==null?void 0:m.stored;p.useEffect(()=>{d||!m||c({bazarr:m.bazarrEnabled,openSubtitles:m.openSubtitlesEnabled,key:"",clearKey:!1,username:m.openSubtitlesUsername??"",password:"",clearLogin:!1})},[m,d]);const h=x=>c(y=>y&&{...y,...x}),j=()=>a("save",async()=>{d&&(await r(()=>F.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())}),b=()=>a("test",async()=>{v(null);const x=await r(()=>F.post("/admin/api/subtitle-test"));v((x==null?void 0:x.results)??[])}),k=()=>a("clear",async()=>{await r(()=>F.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(V,{message:n}),t||!m||!d?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"offered on televisions",value:m.available?"yes":"no",small:!0,icon:"captions",tone:m.available?"ok":void 0},{label:"providers on",value:w((m.bazarrEnabled&&m.bazarrConfigured?1:0)+(m.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:P(f==null?void 0:f.latest),small:!0,icon:"clock"}]}),e.jsxs(he,{cols:"2",children:[e.jsx(T,{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:m.bazarrConfigured?d.bazarr?e.jsx(M,{tone:"ok",children:"on"}):e.jsx(M,{children:"off"}):e.jsx(M,{children:"not configured"}),footer:e.jsx("span",{className:"hint",children:m.bazarrConfigured?`Configured at ${m.bazarrUrl}`:"Set MEMBY_BAZARR_URL and MEMBY_BAZARR_API_KEY to use Bazarr."}),children:e.jsx(z,{label:"Offer Bazarr in the player",hint:"Off leaves every subtitle it has already written in place.",checked:d.bazarr,disabled:!m.bazarrConfigured,onChange:x=>h({bazarr:x})})}),e.jsxs(T,{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:m.openSubtitlesEnabled?e.jsx(M,{tone:m.openSubtitlesAccount?"ok":"warn",children:m.openSubtitlesAccount?"on · signed in":"on · anonymous"}):e.jsx(M,{children:m.openSubtitlesKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(z,{label:"Offer OpenSubtitles in the player",hint:"Needs an API key. It cannot be switched on without one.",checked:d.openSubtitles,onChange:x=>h({openSubtitles:x})}),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:m.openSubtitlesKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:x=>h({key:x.target.value})})}),e.jsx(z,{label:"Remove the saved key",checked:d.clearKey,onChange:x=>h({clearKey:x})}),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:x=>h({username:x.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:x=>h({password:x.target.value})})})]}),e.jsx(z,{label:"Sign out and forget the account",checked:d.clearLogin,onChange:x=>h({clearLogin:x})})]})]}),e.jsxs(T,{children:[e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",busy:o==="save",onClick:()=>void j(),children:"Save subtitle settings"}),e.jsx($,{busy:o==="test",icon:"pulse",onClick:()=>void b(),children:"Test the providers"}),e.jsx("span",{className:"hint",children:m.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."})]}),l===null?null:l.length===0?e.jsx(Y,{children:"No provider is switched on, so there was nothing to ask."}):e.jsx("div",{className:"list",children:l.map(x=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:x.provider}),e.jsx("p",{children:x.message})]}),e.jsx("div",{className:"list-actions",children:e.jsx(M,{tone:x.ok?"ok":"bad",children:x.ok?"reachable":"not reachable"})})]},x.provider))})]}),e.jsx(T,{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(M,{tone:"data",children:[w(f.count)," files · ",Ae(f.bytes)]}):e.jsx(M,{children:"nothing held"}),footer:e.jsx($,{variant:"danger",disabled:!(f!=null&&f.count),onClick:()=>u(!0),children:"Delete every stored subtitle"}),children:e.jsx(e.Fragment,{})})]}),g?e.jsx(xe,{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:o==="clear",onConfirm:()=>void k(),onCancel:()=>u(!1)}):null]})}function bt(){const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(null),[l,v]=p.useState(!1),g=s==null?void 0:s.updatePolicy;p.useEffect(()=>{d||!g||c({version:g.latestVersion??"",url:g.downloadUrl??"",notes:g.notes??"",retireBelow:g.retireBelowVersion??"",required:!!g.minimumVersion&&g.minimumVersion===g.latestVersion,destructive:!!g.retireBelowVersion&&g.retireBelowVersion===g.latestVersion})},[g,d]);const u=j=>a(j?"save":"off",async()=>{d&&(await r(()=>F.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."),v(!1),c(null),await i())}),m=!!(g!=null&&g.minimumVersion)&&(g==null?void 0:g.minimumVersion)===(g==null?void 0:g.latestVersion),f=!!(g!=null&&g.retireBelowVersion)&&(g==null?void 0:g.retireBelowVersion)===(g==null?void 0:g.latestVersion),h=j=>c(b=>b&&{...b,...j});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"App updates",intro:"Publish an optional or a required client update."}),e.jsx(V,{message:n}),t||!d?e.jsx(W,{rows:1}):e.jsxs(T,{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:g!=null&&g.enabled?e.jsxs(M,{tone:m?"warn":"ok",children:[f?"sign-out · ":m?"required · ":"optional · ",g.latestVersion]}):e.jsx(M,{children:"off"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:o==="save",onClick:()=>d.required?v(!0):void u(!0),children:"Save policy"}),e.jsx($,{busy:o==="off",onClick:()=>void u(!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(z,{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(z,{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})})]}),l&&d?e.jsx(xe,{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:o==="save",onConfirm:()=>void u(!0),onCancel:()=>v(!1)}):null]})}const ft=2e4,yt=3e3,wt=[{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 kt(s){const n=[...wt];for(const t of[s.defaultIntervalSeconds,s.intervalSeconds])t>0&&!n.some(i=>i.value===t)&&n.push({value:t,label:es(t).replace(/^every /,"Every ")});return n.sort((t,i)=>t.value-i.value)}function Ns(s){return s==="failed"?"bad":s==="running"?"info":s==="skipped"?"warn":"ok"}function Nt(){const{wrap:s}=ne(),{busy:n,run:t}=X(),[i,r]=p.useState(!1),{data:o,error:a,loading:d,reload:c}=Q("/admin/api/tasks?limit=60",{pollMs:i?yt:ft}),l=(o==null?void 0:o.tasks)??[],v=l.some(y=>y.running);v!==i&&r(v);const g=y=>t(y.id,async()=>{await s(()=>F.post(`/admin/api/tasks/${encodeURIComponent(y.id)}/run`),`${y.name} started.`),await c()}),u=(y,R)=>t(`${y.id}:enabled`,async()=>{await s(()=>F.put(`/admin/api/tasks/${encodeURIComponent(y.id)}`,{enabled:R}),R?`${y.name} switched on.`:`${y.name} switched off.`),await c()}),m=(y,R)=>t(`${y.id}:interval`,async()=>{await s(()=>F.put(`/admin/api/tasks/${encodeURIComponent(y.id)}`,{intervalSeconds:R}),`${y.name} now runs ${es(R)}.`),await c()}),f=y=>t(`${y.id}:interval`,async()=>{await s(()=>F.put(`/admin/api/tasks/${encodeURIComponent(y.id)}`,{intervalSeconds:0}),`${y.name} back to its default cadence.`),await c()}),h=l.filter(y=>{var R;return((R=y.lastRun)==null?void 0:R.status)==="failed"}).length,j=l.filter(y=>y.defaultIntervalSeconds>0&&y.intervalSeconds!==y.defaultIntervalSeconds).length,b=l.filter(y=>!y.enabled).length,k=(o==null?void 0:o.groups)??[],x=l.filter(y=>!y.group);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(V,{message:a}),d?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"Tasks",value:w(l.length),icon:"clock",tone:"info"},{label:"Running now",value:w(l.filter(y=>y.running).length),icon:"pulse",tone:v?"ok":void 0},{label:"Last run failed",value:w(h),icon:"alert",tone:h>0?"bad":void 0},{label:"Switched off",value:w(b),icon:"power",tone:b>0?"warn":void 0},{label:"Retimed",value:w(j),icon:"clock",tone:j>0?"note":void 0}]}),h>0?e.jsx(je,{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,[...k,...x.length>0?[""]:[]].map(y=>{const R=l.filter(S=>S.group===y);return R.length===0?null:e.jsx(T,{title:y||"Other",icon:y==="System"?"chip":y==="Analytics"?"chart":"wrench",tone:y==="System"?"info":y==="Analytics"?"data":"note",children:e.jsx("div",{className:"list",children:R.map(S=>{var H;return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[S.name," ",S.running?e.jsx(M,{tone:"info",children:"running"}):null,S.enabled?null:e.jsx(M,{tone:"warn",children:"off"}),S.defaultIntervalSeconds>0&&S.intervalSeconds!==S.defaultIntervalSeconds?e.jsx(M,{tone:"note",children:"retimed"}):null]}),e.jsx("p",{children:S.description}),e.jsxs("p",{className:"quiet",children:[es(S.intervalSeconds),S.enabled&&S.nextRun?` · next ${be(S.nextRun).replace(" ago","")}`:"",S.lastRun?e.jsxs(e.Fragment,{children:[" · last ",e.jsx("span",{title:P(S.lastRun.startedAt),children:be(S.lastRun.startedAt)}),` in ${Ne(S.lastRun.durationMs)}`,S.lastRun.detail?` — ${S.lastRun.detail}`:""]}):" · never run"]}),(H=S.lastRun)!=null&&H.error?e.jsx("p",{className:"mono",style:void 0,children:e.jsx(M,{tone:"bad",children:S.lastRun.error})}):null]}),e.jsxs("div",{className:"list-actions",children:[S.lastRun?e.jsx(M,{tone:Ns(S.lastRun.status),children:S.lastRun.status}):e.jsx(M,{children:"never run"}),e.jsx("select",{"aria-label":`How often ${S.name} runs`,value:S.intervalSeconds,disabled:n===`${S.id}:interval`||S.running,onChange:E=>void m(S,Number(E.target.value)),children:kt(S).map(E=>e.jsxs("option",{value:E.value,children:[E.label,E.value===S.defaultIntervalSeconds?" (default)":""]},E.value))}),S.defaultIntervalSeconds>0&&S.intervalSeconds!==S.defaultIntervalSeconds?e.jsx($,{size:"sm",icon:"refresh",busy:n===`${S.id}:interval`,onClick:()=>void f(S),children:"Default"}):null,e.jsx(z,{label:"",checked:S.enabled,disabled:n===`${S.id}:enabled`,onChange:E=>void u(S,E)}),e.jsx($,{size:"sm",icon:"play",busy:n===S.id,disabled:S.running,onClick:()=>void g(S),children:"Run now"})]})]},S.id)})})},y||"other")}),e.jsx(T,{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(Z,{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:((o==null?void 0:o.runs.length)??0)===0?e.jsx(ae,{columns:6,children:"No task has run yet."}):o==null?void 0:o.runs.map(y=>{var R;return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(y.startedAt),children:be(y.startedAt)}),e.jsx("td",{children:((R=l.find(S=>S.id===y.taskId))==null?void 0:R.name)??y.taskId}),e.jsx("td",{className:"muted",children:y.trigger}),e.jsx("td",{children:e.jsx(M,{tone:Ns(y.status),children:y.status})}),e.jsx("td",{className:"num muted",children:Ne(y.durationMs)}),e.jsx("td",{className:"muted",children:y.error||y.detail||"—"})]},y.id)})})]})})})]})]})}const St={id:"",name:"Discord",url:"",enabled:!0,events:[]};function Ct(){const{wrap:s,show:n}=ne(),{busy:t,run:i}=X(),{data:r,error:o,loading:a,reload:d}=Q("/admin/api/integrations",{pollMs:6e4}),[c,l]=p.useState(null),[v,g]=p.useState(null),u=(r==null?void 0:r.catalogue)??[],m=(r==null?void 0:r.integrations)??[],f=k=>l({id:k.id,name:k.name,url:"",enabled:k.enabled,events:k.events??[]}),h=()=>i("save",async()=>{if(!c)return;await s(()=>F.post("/admin/api/integrations",c),c.id?"Integration saved.":"Integration added.")&&(l(null),await d())}),j=k=>i("remove",async()=>{await s(()=>F.del(`/admin/api/integrations/${encodeURIComponent(k.id)}`),`${k.name} removed.`),g(null),await d()}),b=k=>i(`test:${k.id}`,async()=>{const x=await s(()=>F.post(`/admin/api/integrations/${encodeURIComponent(k.id)}/test`));x&&n(x.message,x.ok?"ok":"bad"),await d()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Integrations",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:()=>l(St),children:"Add a webhook"})}),e.jsx(V,{message:o}),e.jsx(Mt,{}),e.jsx(Et,{}),e.jsx(At,{}),((r==null?void 0:r.dropped)??0)>0?e.jsxs(je,{tone:"warn",children:[w((r==null?void 0:r.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,a?e.jsx(W,{}):m.length===0&&!c?e.jsx(T,{title:"Nothing configured",icon:"plug",tone:"note",children:e.jsx(Y,{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."})}):m.map(k=>e.jsx(Rt,{integration:k,catalogue:u,busy:t,onEdit:()=>f(k),onTest:()=>void b(k),onRemove:()=>g(k)},k.id)),c?e.jsx($t,{draft:c,catalogue:u,busy:t==="save",onChange:l,onSave:()=>void h(),onCancel:()=>l(null)}):null,v?e.jsx(xe,{title:`Remove ${v.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(v),onCancel:()=>g(null)}):null]})}function Mt(){const{wrap:s}=ne(),{busy:n,run:t}=X(),{data:i,error:r,loading:o,reload:a}=Q("/admin/api/arr-integrations"),d=c=>t("arr-integrations",async()=>{i&&(await s(()=>F.post("/admin/api/arr-integrations",{sonarrEnabled:c.sonarrEnabled??i.sonarrEnabled,radarrEnabled:c.radarrEnabled??i.radarrEnabled}),"Integration settings saved."),await a())});return e.jsxs(T,{title:"Sonarr and Radarr",intro:"Turn either service off without removing its address, API key or request policy. Disabled services are not offered for Memby requests.",icon:"plug",tone:"info",children:[e.jsx(V,{message:r??""}),o?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(z,{label:"Sonarr enabled",hint:i!=null&&i.sonarrConfigured?"Off stops Memby sending or looking up TV requests through Sonarr.":"Sonarr is not configured.",checked:!!(i!=null&&i.sonarrEnabled),disabled:!(i!=null&&i.sonarrConfigured)||n==="arr-integrations",onChange:c=>void d({sonarrEnabled:c})}),e.jsx(z,{label:"Radarr enabled",hint:i!=null&&i.radarrConfigured?"Off stops Memby sending or looking up film requests through Radarr.":"Radarr is not configured.",checked:!!(i!=null&&i.radarrEnabled),disabled:!(i!=null&&i.radarrConfigured)||n==="arr-integrations",onChange:c=>void d({radarrEnabled:c})})]})]})}function Et(){const{wrap:s}=ne(),{busy:n,run:t}=X(),{data:i,error:r,loading:o,reload:a}=Q("/admin/api/sonarr-request-policy"),[d,c]=p.useState(0),[l,v]=p.useState(!1);p.useEffect(()=>{i&&(c(i.qualityProfileId),v(i.searchImmediately))},[i]);const g=()=>t("sonarr-request-policy",async()=>{await s(()=>F.post("/admin/api/sonarr-request-policy",{qualityProfileId:d,searchImmediately:l}),"Sonarr TV request policy saved."),await a()}),u=i==null?void 0:i.profiles.find(m=>m.id===d);return e.jsxs(T,{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(M,{tone:"ok",children:"configured"}):e.jsx(M,{tone:"warn",children:"needs attention"}),footer:e.jsx($,{variant:"primary",busy:n==="sonarr-request-policy",disabled:o||d<=0,onClick:()=>void g(),children:"Save Sonarr policy"}),children:[e.jsx(V,{message:r??(i==null?void 0:i.error)??""}),o?e.jsx(W,{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:m=>c(Number(m.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(m=>e.jsxs("option",{value:m.id,children:[m.name,m.recommended?" — recommended (720p)":""]},m.id))]})})}),e.jsx(z,{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:l,onChange:v}),u?e.jsxs(je,{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 ",l?"start an immediate search.":"not start an immediate search."]}):null]})]})}function At(){const{wrap:s}=ne(),{busy:n,run:t}=X(),{data:i,error:r,loading:o,reload:a}=Q("/admin/api/radarr-request-policy"),[d,c]=p.useState(0),[l,v]=p.useState(!1);p.useEffect(()=>{i&&(c(i.qualityProfileId),v(i.searchImmediately))},[i]);const g=()=>t("radarr-request-policy",async()=>{await s(()=>F.post("/admin/api/radarr-request-policy",{qualityProfileId:d,searchImmediately:l}),"Radarr movie request policy saved."),await a()}),u=i==null?void 0:i.profiles.find(m=>m.id===d);return e.jsxs(T,{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(M,{tone:"ok",children:"configured"}):e.jsx(M,{tone:"warn",children:"needs attention"}),footer:e.jsx($,{variant:"primary",busy:n==="radarr-request-policy",disabled:o||d<=0,onClick:()=>void g(),children:"Save Radarr policy"}),children:[e.jsx(V,{message:r??(i==null?void 0:i.error)??""}),o?e.jsx(W,{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:m=>c(Number(m.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(m=>e.jsxs("option",{value:m.id,children:[m.name,m.recommended?" — recommended (720p)":""]},m.id))]})})}),e.jsx(z,{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:l,onChange:v}),u?e.jsxs(je,{tone:"info",children:["Requested films will use ",e.jsx("b",{children:u.name})," (profile ID ",u.id,"), remain monitored, and ",l?"start an immediate search.":"not start an immediate search."]}):null]})]})}function Rt({integration:s,catalogue:n,busy:t,onEdit:i,onTest:r,onRemove:o}){const a=s.health,d=!a.lastFailure||a.lastSuccess&&a.lastSuccess>a.lastFailure,c=s.events??[];return e.jsxs(T,{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(M,{tone:"ok",children:"on"}):e.jsx(M,{tone:"warn",children:"off"}),a.deliveries>0?e.jsx(M,{tone:d?"ok":"bad",children:d?"delivering":"failing"}):e.jsx(M,{children:"never used"}),e.jsx($,{size:"sm",icon:"pulse",busy:t===`test:${s.id}`,onClick:r,children:"Test"}),e.jsx($,{size:"sm",onClick:i,children:"Edit"}),e.jsx($,{size:"sm",variant:"danger",icon:"trash",onClick:o,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(l=>{var v;return((v=n.find(g=>g.type===l))==null?void 0:v.label)??l}).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:a.lastSuccess?P(a.lastSuccess):"never"})]}),e.jsx("div",{className:"list-actions",children:a.deliveries>0?e.jsxs("span",{className:"quiet",children:[w(a.deliveries)," attempts, ",w(a.failures)," failed"]}):null})]}),a.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:[P(a.lastFailure),a.lastError?` — ${a.lastError}`:""]})]})}):null]}),s.deliveries.length>0?e.jsx(Z,{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(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(l.attemptedAt),children:be(l.attemptedAt)}),e.jsx("td",{className:"muted",children:l.eventType}),e.jsx("td",{children:l.success?e.jsx(M,{tone:"ok",children:l.statusCode||"ok"}):e.jsx(M,{tone:"bad",children:l.error||l.statusCode||"failed"})}),e.jsx("td",{className:"num muted",children:Ne(l.durationMs)})]},l.id))})]})}):e.jsx(Z,{children:e.jsx("table",{children:e.jsx("tbody",{children:e.jsx(ae,{columns:4,children:"Nothing has been delivered through this webhook yet."})})})})]})}function $t({draft:s,catalogue:n,busy:t,onChange:i,onSave:r,onCancel:o}){const a=[...new Set(n.map(c=>c.group))],d=(c,l)=>i({...s,events:l?[...s.events,c]:s.events.filter(v=>v!==c)});return e.jsxs(T,{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:r,children:s.id?"Save":"Add"}),e.jsx($,{variant:"quiet",onClick:o,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=>i({...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=>i({...s,url:c.target.value})})})]}),e.jsx(z,{label:"Enabled",hint:"Off keeps the configuration and stops the posts.",checked:s.enabled,onChange:c=>i({...s,enabled:c})}),a.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(l=>l.group===c).map(l=>e.jsx(z,{label:l.label,hint:l.description,checked:s.events.includes(l.type),onChange:v=>d(l.type,v)},l.type))]},c))]})}function Tt(){var G,ee,se,ue;const{status:s,error:n,loading:t,reload:i}=oe(),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(""),[l,v]=p.useState(!1),[g,u]=p.useState(!1),[m,f]=p.useState(!1),[h,j]=p.useState("23:00"),[b,k]=p.useState("07:00"),[x,y]=p.useState(""),[R,S]=p.useState(!1),H=!!((G=s==null?void 0:s.maintenance)!=null&&G.enabled);p.useEffect(()=>{var A;!g&&s&&c(((A=s.maintenance)==null?void 0:A.message)??"")},[s,g]),p.useEffect(()=>{R||!(s!=null&&s.quietTime)||(f(s.quietTime.enabled),j(s.quietTime.startTime),k(s.quietTime.endTime),y(s.quietTime.message))},[s,R]);const E=A=>a(A?"on":"off",async()=>{await r(()=>F.post("/admin/api/maintenance",{enabled:A,message:d}),A?"Memby is offline for every television.":"Memby is back online."),v(!1),u(!1),await i()}),I=()=>a("quiet",async()=>{await r(()=>F.post("/admin/api/quiet-time",{enabled:m,startTime:h,endTime:b,message:x}),m?"Quiet time saved.":"Quiet time turned off."),S(!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(V,{message:n}),t?e.jsx(W,{rows:1}):e.jsx(T,{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:H?"bad":"warn",actions:H?e.jsx(M,{tone:"bad",children:"offline"}):e.jsx(M,{tone:"ok",children:"online"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"danger",disabled:H,onClick:()=>v(!0),children:"Go offline"}),e.jsx($,{disabled:!H,busy:o==="off",onClick:()=>void E(!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:A=>{c(A.target.value),u(!0)}})})}),t?null:e.jsxs(T,{title:"Quiet time",intro:`Pause new television requests and server background work every day in ${((ee=s==null?void 0:s.quietTime)==null?void 0:ee.timeZone)??"the household 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:(se=s==null?void 0:s.quietTime)!=null&&se.active?"warn":"info",actions:(ue=s==null?void 0:s.quietTime)!=null&&ue.active?e.jsx(M,{tone:"warn",children:"active now"}):m?e.jsx(M,{tone:"ok",children:"scheduled"}):e.jsx(M,{children:"off"}),footer:e.jsx($,{variant:"primary",busy:o==="quiet",onClick:()=>void I(),children:"Save quiet time"}),children:[e.jsx(z,{label:"Pause server activity during quiet time",checked:m,onChange:A=>{f(A),S(!0)}}),e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Starts",hint:"Uses the household's 24-hour clock.",children:e.jsx("input",{type:"time",value:h,onChange:A=>{j(A.target.value),S(!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:b,onChange:A=>{k(A.target.value),S(!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:x,placeholder:"Quiet time — try again after 7 am",onChange:A=>{y(A.target.value),S(!0)}})})]}),l?e.jsx(xe,{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:o==="on",onConfirm:()=>void E(!0),onCancel:()=>v(!1)}):null]})}const It=-1;function De(s){return s===0?"":s<0?"off":String(s)}function Fe(s,n){const t=s.trim().toLowerCase();if(t==="")return 0;if(n&&(t==="off"||t==="none"||t==="0"))return It;const i=Number.parseInt(t,10);return Number.isFinite(i)?i:0}function Pe(s,n){return s<=0?"off":`${s} ${n}${s===1?"":"s"}`}function Ye(s){return{timezone:s.timezone??"",logLevel:s.logLevel??"",sessionIdleDays:De(s.sessionIdleDays),sonarrAlertMinutes:De(s.sonarrAlertMinutes),radarrAlertMinutes:De(s.radarrAlertMinutes),embyHealthSeconds:De(s.embyHealthSeconds)}}function Lt(){const{data:s,error:n,loading:t,reload:i}=Q("/admin/api/gateway-settings"),{wrap:r}=ne(),{busy:o,run:a}=X(),[d,c]=p.useState(null);p.useEffect(()=>{!d&&s&&c(Ye(s.settings))},[s,d]);const l=(h,j)=>c(b=>b&&{...b,[h]:j}),v=()=>a("save",async()=>{if(!d)return;const h={timezone:d.timezone.trim(),logLevel:d.logLevel.trim(),sessionIdleDays:Fe(d.sessionIdleDays,!1),sonarrAlertMinutes:Fe(d.sonarrAlertMinutes,!0),radarrAlertMinutes:Fe(d.radarrAlertMinutes,!0),embyHealthSeconds:Fe(d.embyHealthSeconds,!0)},j=await r(()=>F.post("/admin/api/gateway-settings",h),"Gateway settings saved.");j&&c(Ye(j.settings)),await i()}),g=()=>a("clear",async()=>{const h=await r(()=>F.post("/admin/api/gateway-settings",{timezone:"",logLevel:"",sessionIdleDays:0,sonarrAlertMinutes:0,radarrAlertMinutes:0,embyHealthSeconds:0}),"Every setting is back to what this container was deployed with.");h&&c(Ye(h.settings)),await i()}),u=s==null?void 0:s.deployed,m=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(V,{message:n}),t||!d||!u||!m?e.jsx(W,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(T,{title:"This gateway",intro:"What the process is running and what it currently believes.",icon:"chip",tone:"info",actions:e.jsx(M,{tone:"info",children:(s==null?void 0:s.version)??"unknown"}),children:e.jsx(ss,{rows:[{label:"Household timezone",value:m.timezone||"not set"},{label:"Log level",value:m.logLevel},{label:"Sign-in expiry",value:Pe(m.sessionIdleDays,"day")},{label:"Emby health probe",value:Pe(m.embyHealthSeconds,"second")},{label:"Episode alert window",value:Pe(m.sonarrAlertMinutes,"minute")},{label:"Film alert window",value:Pe(m.radarrAlertMinutes,"minute")}]})}),e.jsxs(T,{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:o==="save",onClick:()=>void v(),children:"Save settings"}),e.jsx($,{busy:o==="clear",onClick:()=>void g(),children:"Use deployed values"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(q,{label:"Household 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:h=>l("timezone",h.target.value)})}),e.jsx(q,{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:h=>l("logLevel",h.target.value),children:[e.jsxs("option",{value:"",children:["Deployed (",u.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: ${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:h=>l("sessionIdleDays",h.target.value)})}),e.jsx(q,{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:h=>l("embyHealthSeconds",h.target.value)})})]}),e.jsxs("div",{className:"fields",children:[e.jsx(q,{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:h=>l("sonarrAlertMinutes",h.target.value)})}),e.jsx(q,{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:h=>l("radarrAlertMinutes",h.target.value)})})]}),e.jsxs(je,{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(je,{children:["Last changed by ",s.settings.updatedBy,s.settings.updatedAt?` on ${new Date(s.settings.updatedAt).toLocaleString("en-NZ")}`:"","."]}):null]})]})]})}function qt(){const{status:s,error:n,loading:t}=oe(),i=(s==null?void 0:s.runs)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Imports",intro:"Catalogue synchronisation history."}),e.jsx(V,{message:n}),t?e.jsx(W,{rows:1}):e.jsx(T,{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.",icon:"sync",tone:"data",children:e.jsx(Z,{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(ae,{columns:8,children:"Nothing has been imported yet."}):i.map(r=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(r.startedAt)}),e.jsx("td",{children:r.kind}),e.jsx("td",{className:"muted",children:r.trigger}),e.jsx("td",{children:e.jsx(M,{tone:r.status==="success"?"ok":r.status==="running"?"warn":"bad",children:r.status})}),e.jsx("td",{className:"num",children:w(r.itemsSeen)}),e.jsx("td",{className:"num",children:w(r.itemsUpserted)}),e.jsx("td",{className:"num",children:w(r.itemsRemoved)}),e.jsx("td",{className:"muted",children:r.error||""})]},r.id||r.startedAt))})]})})})]})}const Dt={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"},sn=s=>Dt[s]??"quiet",Ft={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"]},Pt=[[/^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 Ot(s,n){const t=n.toLowerCase();for(const[r,o]of Pt)if(r.test(t))return o;const i=Ft[s];return i||(s?["gateway","Gateway",Ke(s.replace(/[-_]/g," "))]:["gateway","Gateway","Server"])}const _t={h:36e5,m:6e4,s:1e3,ms:1,us:.001,µs:.001,ns:1e-6};function ts(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 r of n){const o=r[2]?_t[r[2]]:void 0;o!==void 0&&(t+=Number(r[1])*o,i=!0)}return i?t:null}function os(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 Ut=s=>s>=3e3?"bad":s>=1e3?"warn":null,Vt={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 Bt(s){const n=Vt[s];return n||(s>=500?"Server Error":s>=400?"Client Error":s>=300?"Redirected":s>=200?"OK":"Response")}const Wt=s=>s>=500?"bad":s>=400?"warn":s>=300?"quiet":"ok",Ht={"/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"},zt=s=>/\d/.test(s)||s.length>24;function Ss(s){const n=Ht[s];if(n)return n;const t=s.split("/").filter(r=>r&&r!=="v1"&&r!=="api");t[0]==="admin"&&t.shift();const i=t.filter(r=>!zt(r));return i.length===0?s:Ke(i.join(" ").replace(/[-_.]/g," ").replace(/\s+/g," ").trim())}const Ke=s=>s&&s.charAt(0).toUpperCase()+s.slice(1),Oe=s=>Ke(s.replace(/_/g," ")),de=s=>s==null?"":String(s),nn=new Set(["","unknown","none","null","","0"]),ge=s=>!nn.has(de(s).toLowerCase()),Kt=["title","series","name","query","item_title","file"],Gt={directplay:"ok",direct:"ok",directstream:"ok",transcode:"warn",transcoding:"warn"};function Zt(s,n,t){if(t!==null)return{label:`${t} ${Bt(t)}`,short:String(t),tone:Wt(t)};if(ge(s.error))return{label:"Failed",short:"Failed",tone:n==="WARN"?"warn":"bad"};const i=de(s.play_method).toLowerCase().replace(/[\s_-]/g,"");if(i&&!nn.has(i)){const r=Ke(de(s.play_method).replace(/([a-z])([A-Z])/g,"$1 $2"));return{label:r,short:r,tone:Gt[i]??"info"}}if(ge(s.cache)){const r=/hit|true|yes/i.test(de(s.cache));return{label:r?"Cached":"Cache miss",short:r?"Cached":"Miss",tone:r?"data":"quiet"}}return n==="ERROR"?{label:"Failed",short:"Failed",tone:"bad"}:n==="WARN"?{label:"Warning",short:"Warning",tone:"warn"}:null}function Jt(s){const n=[];ge(s.user)&&n.push(de(s.user)),ge(s.device)&&n.push(de(s.device));const t=ts(s.marker_ms);t!==null&&t>0&&n.push(`Start ${Cs(t)}`);const i=ts(s.position);return i!==null&&i>0&&n.push(`At ${Cs(i)}`),ge(s.watched)&&n.push(`${de(s.watched)} watched`),ge(s.reason)&&n.push(de(s.reason)),n.slice(0,3).join(" · ")}function Cs(s){const n=Math.round(s/1e3),t=Math.floor(n/3600),i=Math.floor(n%3600/60),r=n%60,o=a=>String(a).padStart(2,"0");return t>0?`${t}:${o(i)}:${o(r)}`:`${i}:${o(r)}`}const tn=[{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:"Diagnostics",keys:["error","stack","correlation","version","gateway_version","duration"]}],Yt=new Set(tn.flatMap(s=>s.keys)),Qt=s=>Yt.has(s),Xt=new Intl.DateTimeFormat("en-NZ",{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),ei=new Intl.DateTimeFormat("en-NZ",{weekday:"short",day:"numeric",month:"short"}),Ms=new WeakMap;function Re(s){const n=Ms.get(s);if(n)return n;const t=si(s);return Ms.set(s,t),t}function si(s){const n=s.attributes??{},t=s.message??"",[i,r,o]=Ot(de(n.component),t),a=de(n.path),d=a?de(n.method).toUpperCase():"",c=Number(n.status),l=a&&Number.isFinite(c)&&c>0?c:null,v=t==="request"&&!!a,g=Kt.find(R=>ge(n[R])),u=g?de(n[g]):"";let m,f;v?(m=d||"HTTP",f=Ss(a)):a&&d?(m=d,f=u?`${Oe(t)} · ${u}`:`${Oe(t)} — ${Ss(a)}`):(m="",f=u?`${Oe(t)} · ${u}`:Oe(t));const h=ts(n.duration??n.duration_ms??n.negotiation_duration),j=ge(n.error)?de(n.error):"",b=Jt(n),k=new Date(s.occurredAt),x=Object.entries(n);return{serviceKey:i,service:r,component:o,action:m,summary:f,context:b,detail:j,result:Zt(n,s.level,l),durationMs:h,method:d,status:l,eventKey:t,level:s.level,time:`${Xt.format(k)}.${String(k.getMilliseconds()).padStart(3,"0")}`,day:ei.format(k),dayKey:k.toDateString(),tall:!!j||!!b&&!v,haystack:[t,r,o,f,b,j,...x.flat().map(de)].join(" ").toLowerCase(),fields:x,attributes:n}}const Es={TRACE:5,DEBUG:10,INFO:20,WARN:30,ERROR:40},Qe={level:"INFO",service:"",component:"",event:"",method:"",status:"",slower:0,text:""};function ni(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 ti(s,n,t){if((Es[s.level]??0)<(Es[n.level]??20))return!1;const i=Re(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||!ni(n.status,i.status)||n.slower>0&&(i.durationMs??0)({key:o,label:a})).sort((o,a)=>o.label.localeCompare(a.label)),components:[...t].sort((o,a)=>o.localeCompare(a)),events:[...i].sort((o,a)=>o.localeCompare(a)),methods:[...r].sort((o,a)=>o.localeCompare(a))}}function ai(s,n){var i;const t=[];if(s.service){const r=((i=n.find(o=>o.key===s.service))==null?void 0:i.label)??s.service;t.push({key:"service",label:`Service: ${r}`})}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: >${os(s.slower)}`}),s.text&&t.push({key:"text",label:`Search: ${s.text}`}),t}const _e=2e4,ri=5e3,As=30,Rs=48,$s=26,li=31,Ts=10,oi=[{value:"TRACE",label:"Everything"},{value:"DEBUG",label:"Debug+"},{value:"INFO",label:"Info+"},{value:"WARN",label:"Warnings+"},{value:"ERROR",label:"Errors only"}],ci=[{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)"}],di=[{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"}],Is=s=>s.replace(/_/g," ");function Ue({onPick:s,className:n,title:t,children:i,...r}){return e.jsx("button",{type:"button",className:`logfacet ${n}`,title:t,onClick:s,...r,children:i})}const hi=p.memo(function({event:n,view:t,top:i,height:r,selected:o,onInspect:a,onFilter:d}){const c=t.durationMs!==null?Ut(t.durationMs):null;return e.jsxs("div",{className:"logrow","data-level":t.level,"data-selected":o||void 0,style:{transform:`translateY(${i}px)`,height:`${r}px`},children:[e.jsx("time",{className:"logrow-time",title:n.occurredAt,children:t.time}),e.jsx(Ue,{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(Ue,{className:"logrow-service","data-tone":sn(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(Ue,{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.detail||t.summary,onClick:()=>a(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.detail?e.jsxs("span",{className:"logrow-error",children:["↳ ",t.detail]}):t.context?e.jsx("span",{className:"logrow-context",children:t.context}):null]}),e.jsx("span",{className:"logrow-result",children:t.result?e.jsx(Ue,{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?os(t.durationMs):""})]})});function ui({event:s,view:n,onClose:t}){const[i,r]=p.useState(!1),o=n.fields.filter(([c])=>!Qt(c)&&c!=="component"),a=async()=>{try{await navigator.clipboard.writeText(JSON.stringify(s,null,2)),r(!0),window.setTimeout(()=>r(!1),1600)}catch{r(!1)}},d=tn.map(c=>({title:c.title,rows:c.keys.map(l=>[l,n.attributes[l]]).filter(([,l])=>l!=null&&String(l)!=="")})).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":sn(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:a,icon:"download",children:i?"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:os(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(([l,v])=>e.jsxs(p.Fragment,{children:[e.jsx("dt",{children:Is(l)}),e.jsx("dd",{children:String(v)})]},l))})]},c.title)),o.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Details"}),e.jsx("dl",{children:o.map(([c,l])=>e.jsxs(p.Fragment,{children:[e.jsx("dt",{children:Is(c)}),e.jsx("dd",{children:String(l)})]},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 mi(){var ds;const[s,n]=p.useState([]),[t,i]=p.useState(0),[r,o]=p.useState(!1),[a,d]=p.useState(0),[c,l]=p.useState(Qe),[v,g]=p.useState(""),[u,m]=p.useState({top:0,height:600}),[f,h]=p.useState(!0),[j,b]=p.useState(null),k=p.useDeferredValue(c.text.trim().toLowerCase()),x=p.useRef(0),y=p.useRef(!1),R=p.useRef(0),S=p.useRef(null),H=p.useRef(!0),E=p.useRef(void 0),I=p.useRef([]),G=p.useRef(r);p.useEffect(()=>{G.current=r},[r]);const ee=p.useCallback(C=>{n(O=>{const K=O.concat(C);return K.length>_e?K.slice(K.length-_e):K})},[]),se=p.useCallback(async()=>{if(y.current||document.hidden)return;y.current=!0;const C=R.current,O=[];let K=0;try{let ce=0,ve;do ve=await F.get(`/admin/api/events?after=${x.current}&limit=1000`),x.current=ve.next||x.current,K+=ve.dropped||0,O.push(...ve.events??[]),ce+=1;while(ve.hasMore&&ce<20);g("")}catch(ce){g(ce instanceof Error?ce.message:String(ce))}finally{O.length>0&&C===R.current&&(G.current?(I.current=I.current.concat(O),I.current.length>_e&&(I.current=I.current.slice(I.current.length-_e)),d(I.current.length)):ee(O)),K>0&&C===R.current&&i(ce=>ce+K),y.current=!1}},[ee]);p.useEffect(()=>{let C;const O=()=>{window.clearInterval(C),C=document.hidden?void 0:window.setInterval(()=>void se(),ri)},K=()=>{O(),document.hidden||se()};return se(),O(),document.addEventListener("visibilitychange",K),()=>{window.clearInterval(C),document.removeEventListener("visibilitychange",K)}},[se]);const ue=p.useCallback(()=>{G.current=!0,o(!0)},[]),A=p.useCallback(()=>{G.current=!1;const C=I.current;I.current=[],d(0),o(!1),C.length&&ee(C)},[ee]),L=p.useCallback(C=>{l(O=>({...O,...C}))},[]),N=p.useMemo(()=>s.filter(C=>ti(C,c,k)),[s,c,k]),D=p.useMemo(()=>{const C=new Float64Array(N.length+1),O=new Uint8Array(N.length),K=new Uint8Array(N.length);let ce=0,ve="";for(let Se=0;Se{let C=0,O=N.length;for(;C>1;(D.tops[K]??0)+(D.heights[K]??0)<=_?C=K+1:O=K}return Math.max(0,C-Ts)},[D,_,N.length]),pe=p.useMemo(()=>{const C=_+u.height;let O=te;for(;O{const C=[];for(let O=te;Os.find(C=>C.sequence===j),[s,j]),Ge=p.useCallback(()=>{const C=S.current;C&&(H.current=!0,C.scrollTop=C.scrollHeight,h(!0),m({top:C.scrollTop,height:C.clientHeight}))},[]);p.useLayoutEffect(()=>{const C=S.current;!C||!H.current||(C.scrollTop=C.scrollHeight,m({top:C.scrollTop,height:C.clientHeight}))},[Le,D.total]),p.useEffect(()=>()=>window.cancelAnimationFrame(E.current??0),[]);const ln=()=>{const C=S.current;if(!C)return;const O=C.scrollHeight-C.scrollTop-C.clientHeight{m({top:C.scrollTop,height:C.clientHeight})})},on=()=>{const C=new Blob([JSON.stringify(N,null,2)],{type:"application/json"}),O=document.createElement("a");O.href=URL.createObjectURL(C),O.download=`memby-events-${new Date().toISOString().replace(/[:.]/g,"-")}.json`,O.click(),window.setTimeout(()=>URL.revokeObjectURL(O.href),1e3)},Me=p.useMemo(()=>ii(s),[s]),cs=ai(c,Me.services);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Server logs",intro:"Structured gateway events as they happen."}),e.jsx(V,{message:v}),e.jsxs(T,{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=>L({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=>L({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=>L({level:C.target.value}),children:oi.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Event",value:c.event,onChange:C=>L({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=>L({status:C.target.value}),children:ci.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Method",value:c.method,onChange:C=>L({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=>L({slower:Number(C.target.value)}),children:di.map(C=>e.jsx("option",{value:String(C.value),children:C.label},C.value))}),e.jsxs("label",{className:"logsearch",children:[e.jsx(J,{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=>L({text:C.target.value})})]})]}),e.jsxs("div",{className:"logbar-actions",children:[e.jsx($,{size:"sm",variant:"quiet",onClick:()=>r?A():ue(),icon:r?"play":"clock",children:r?a?`Resume (${w(a)})`:"Resume":"Pause"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{R.current+=1,I.current=[],d(0),n([]),i(0),b(null)},children:"Clear view"}),e.jsx($,{size:"sm",variant:"quiet",onClick:on,icon:"download",title:"Export the rows matching the current filters, as delivered by the gateway",children:"Export JSON"})]})]}),cs.length?e.jsxs("div",{className:"logchips",children:[cs.map(C=>e.jsxs("button",{type:"button",className:"logchip",onClick:()=>L({[C.key]:Qe[C.key]}),children:[C.label,e.jsx(J,{name:"close"})]},C.key)),e.jsx("button",{type:"button",className:"logchip logchip-clear",onClick:()=>l(Qe),children:"Clear all"})]}):null,e.jsxs("div",{className:"logshell",children:[e.jsxs("div",{className:"logview",ref:S,onScroll:ln,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"})]}),N.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:`${D.total}px`},children:Ie.map(({event:C,view:O,index:K})=>e.jsxs(p.Fragment,{children:[D.divider[K]?e.jsx("div",{className:"logday",style:{transform:`translateY(${(D.tops[K]??0)-$s}px)`},children:e.jsx("span",{children:O.day})}):null,e.jsx(hi,{event:C,view:O,top:D.tops[K]??0,height:D.heights[K]??As,selected:C.sequence===j,onInspect:b,onFilter:L})]},C.sequence))})]}),!f&&N.length>0?e.jsxs("button",{type:"button",className:"logtail",onClick:Ge,children:[e.jsx(J,{name:"caret"}),"Jump to latest"]}):null]}),e.jsxs("p",{className:"hint",children:[w(s.length)," retained · ",w(N.length)," matching",N.length?` · ${w(Ie.length)} rows mounted`:"",t?` · ${w(t)} overwritten before delivery`:"",r?` · paused${a?`, ${w(a)} held`:""}`:""]}),me?e.jsx(ui,{event:me,view:Re(me),onClose:()=>b(null)}):s.length?e.jsx(je,{children:"Select a row to see the full record — request, context, diagnostics and raw event."}):null]})]})}const Ls=["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 ye(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function pi(){const[s,n]=p.useState(30),[t,i]=p.useState(""),r=is(),o=p.useMemo(()=>`/admin/api/journeys${Te({days:s,userId:t})}`,[s,t]),{data:a,error:d,loading:c}=Q(o),l=a==null?void 0:a.stats,v=(a==null?void 0:a.users)??[],g=(a==null?void 0:a.actions)??[],u=(a==null?void 0:a.paths)??[],m=u[0],f=p.useMemo(()=>{const h=new Map(((a==null?void 0:a.features)??[]).map(b=>[b.feature,b])),j=new Map(Ls.map((b,k)=>[b,k]));return[...new Set([...Ls,...h.keys()])].map(b=>({name:b,stat:h.get(b)})).sort((b,k)=>{var y,R;const x=(((y=k.stat)==null?void 0:y.uses)??0)-(((R=b.stat)==null?void 0:R.uses)??0);return x||(j.get(b.name)??Number.MAX_SAFE_INTEGER)-(j.get(k.name)??Number.MAX_SAFE_INTEGER)})},[a==null?void 0:a.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(V,{message:d}),e.jsx(T,{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;i(j),j&&r(`/admin/journeys/${encodeURIComponent(j)}`)},children:[e.jsx("option",{value:"",children:"All users"}),v.map(h=>e.jsx("option",{value:h.userId,children:h.username||h.userId},h.userId))]})})]}),children:c?e.jsx(W,{rows:1}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:"journeys",value:w(l==null?void 0:l.journeys),icon:"list",tone:"data"},{label:"viewers",value:w(l==null?void 0:l.viewers),icon:"people",tone:"info"},{label:"completion",value:Ve(l==null?void 0:l.completionRate),icon:"check",tone:"ok"},{label:"abandoned",value:w(l==null?void 0:l.abandoned),icon:"alert",tone:"note"},{label:"active now",value:w(l==null?void 0:l.active),icon:"pulse",tone:"info"},{label:"average steps",value:((l==null?void 0:l.averageSteps)??0).toFixed(1),icon:"chart"},{label:"average visit",value:Ne(l==null?void 0:l.averageTimeMs),small:!0,icon:"clock"},{label:"history kept",value:`${(a==null?void 0:a.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:Ve(l==null?void 0:l.completionRate)})]}),e.jsx(In,{value:(l==null?void 0:l.completed)??0,total:(l==null?void 0:l.journeys)??0}),e.jsxs("p",{children:[w(l==null?void 0:l.completed)," completed · ",w(l==null?void 0:l.abandoned)," abandoned ·"," ",w(l==null?void 0:l.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:m?`${ye(m.from)} → ${ye(m.to)}`:"Not enough data"}),e.jsx("p",{children:m?`${w(m.count)} times in this window`:"Journeys will appear here as viewers move through Memby."})]})]})]})}),e.jsxs(he,{cols:"2",children:[e.jsx(T,{title:"What people do",intro:"Actions show total use and how many separate visits included them.",icon:"chart",tone:"info",children:e.jsx(Z,{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:g.length===0?e.jsx(ae,{columns:3,children:"No significant actions in this window."}):g.map(h=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:ye(h.action)}),e.jsx("span",{className:"table-sub",children:ye(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(T,{title:"Where people go",intro:"The most common steps between screens, including where quiet visits ended.",icon:"list",tone:"note",children:e.jsx(Z,{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(ae,{columns:2,children:"No repeated paths in this window."}):u.map((h,j)=>e.jsxs("tr",{children:[e.jsxs("td",{children:[ye(h.from)," ",e.jsx("span",{className:"route-arrow",children:"→"})," ",ye(h.to)]}),e.jsx("td",{className:"num",children:w(h.count)})]},`${h.from}:${h.to}:${j}`))})]})})})]}),e.jsx(T,{title:"Feature use",intro:"Rare and unused features are shown against Memby's major feature catalogue.",icon:"pulse",tone:"data",children:e.jsx(Z,{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 b=(j==null?void 0:j.uses)??0;return e.jsxs("tr",{children:[e.jsx("td",{children:ye(h)}),e.jsx("td",{className:"num",children:w(b)}),e.jsx("td",{className:"muted nowrap",children:j?P(j.lastUsedAt):"—"}),e.jsx("td",{children:b===0?e.jsx(M,{tone:"warn",children:"not used"}):b<3?e.jsx(M,{tone:"note",children:"rare"}):e.jsx(M,{tone:"ok",children:"used"})})]},h)})})]})})}),t?null:e.jsx(T,{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 fe=s=>{const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n},an=s=>fe((s==null?void 0:s.target)||(s==null?void 0:s.screen)||(s==null?void 0:s.source)||(s==null?void 0:s.feature)),xi=s=>{const n=s.find(t=>t.category==="playback"&&t.action==="request");return n!=null&&n.source?fe(n.source):an(s[0])},qs=s=>s.itemName?`${fe(s.itemType)} · ${s.itemName}`:s.source&&s.target?`${fe(s.source)} → ${fe(s.target)}`:an(s),ji=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]??fe(s.action);function vi(s){var i;const n=[...s].reverse().find(r=>r.category==="playback"&&r.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(r=>r.outcome))==null?void 0:i.outcome;return t==="success"||t==="completed"?{label:fe(t),tone:"ok"}:t==="failure"||t==="cancelled"||t==="abandoned"?{label:fe(t),tone:"note"}:s.some(r=>r.action==="stop"&&r.category==="playback")?{label:"watched",tone:"ok"}:{label:"left before playback ended",tone:"warn"}}function gi(s){const n=s.reduce((t,i,r)=>(i.category==="playback"&&i.action==="request"&&t.push(r),t),[]);return n.length===0?[s]:n.map((t,i)=>s.slice(i===0?0:t,n[i+1]??s.length))}function bi(){var c,l;const{userId:s=""}=We(),n=p.useMemo(()=>`/admin/api/journeys${Te({days:90,userId:s})}`,[s]),{data:t,error:i,loading:r}=Q(n),o=((l=(c=t==null?void 0:t.users)==null?void 0:c.find(v=>v.userId===s))==null?void 0:l.username)||s,a=p.useMemo(()=>{const v=new Map;for(const g of(t==null?void 0:t.events)??[])v.set(g.journeyId,[...v.get(g.journeyId)??[],g]);return[...v.values()].map(g=>g.sort((u,m)=>u.sequence-m.sequence)).sort((g,u)=>{var m,f;return(((m=u[0])==null?void 0:m.occurredAt)??"").localeCompare(((f=g[0])==null?void 0:f.occurredAt)??"")})},[t==null?void 0:t.events]),d=a.flatMap(v=>gi(v).map((g,u)=>{var m;return{events:g,key:`${(m=v[0])==null?void 0:m.journeyId}:${u}`}}));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:`${o}'s journeys`,intro:"Each app session is shown as the viewing journeys it contains: entry, selection, playback outcome.",icon:"journey",crumbs:e.jsx(re,{className:"crumb",to:"/admin/journeys",children:"Journeys"})}),e.jsx(V,{message:i}),r?e.jsx(W,{}):e.jsx(T,{title:"Viewing journeys",intro:`${a.length} app session${a.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((v,g)=>{const u=v.events,m=u[0],f=[...u].reverse().find(j=>j.itemName||j.action==="select"||j.category==="playback"&&j.action==="request"),h=vi(u);return e.jsxs("article",{className:"visit",children:[e.jsxs("header",{children:[e.jsxs("div",{children:[e.jsx("b",{children:P(m==null?void 0:m.occurredAt)}),e.jsxs("span",{children:["Journey ",g+1," · ",u.length," recorded steps"]})]}),e.jsx(M,{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(J,{name:"journey"}),e.jsx("span",{children:"Entered from"}),e.jsx("b",{children:xi(u)})]}),e.jsxs("div",{className:"journey-answer","data-kind":"selection",children:[e.jsx(J,{name:"play"}),e.jsx("span",{children:"Selected"}),e.jsx("b",{children:f?qs(f):"Nothing selected"})]}),e.jsxs("div",{className:"journey-answer","data-kind":"outcome",children:[e.jsx(J,{name:h.tone==="ok"?"check":"clock"}),e.jsx("span",{children:"Outcome"}),e.jsx("b",{children:h.label})]})]}),e.jsx("ol",{className:"journey-timeline",children:u.map(j=>e.jsxs("li",{children:[e.jsx("span",{className:"timeline-dot","data-action":j.action}),e.jsxs("div",{children:[e.jsx("b",{children:ji(j)}),e.jsx("span",{children:qs(j)})]}),e.jsx("time",{children:P(j.occurredAt)})]},`${j.journeyId}:${j.sequence}`))})]},v.key)})})})]})}function Ds(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function fi(){const[s,n]=p.useState(30),{data:t,error:i,loading:r}=Q(`/admin/api/analytics?days=${s}`),o=(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(V,{message:i}),e.jsx(T,{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:a=>n(Number(a.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:r?e.jsx(W,{rows:1}):e.jsx(Z,{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:o.length===0?e.jsx(ae,{columns:8,children:"No events in this window."}):o.map(a=>e.jsxs("tr",{children:[e.jsx("td",{children:Ds(a.rowId)}),e.jsx("td",{className:"muted",children:Ds(a.rowKind)}),e.jsx("td",{className:"num",children:Ne(a.dwellMs)}),e.jsx("td",{className:"num",children:w(a.impressions)}),e.jsx("td",{className:"num",children:w(a.focuses)}),e.jsx("td",{className:"num",children:w(a.selects)}),e.jsx("td",{className:"num",children:Ve(a.selectRate)}),e.jsx("td",{className:"num",children:w(a.viewers)})]},`${a.rowId}:${a.rowKind}`))})]})})})]})}function yi(){const[s,n]=p.useState(7),{data:t,error:i,loading:r}=Q(`/admin/api/searches?days=${s}`),o=(t==null?void 0:t.terms)??[],a=(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 the household has been looking for, and what it searched just now."}),e.jsx(V,{message:i}),e.jsx(le,{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"}]}),r?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(T,{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(Z,{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:o.length===0?e.jsx(ae,{columns:4,children:"Nothing searched in this window."}):o.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:P(c.lastAt)})]},c.query))})]})})}),e.jsx(T,{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(Z,{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:a.length===0?e.jsx(ae,{columns:3,children:"No searches in this window."}):a.map((c,l)=>e.jsxs("tr",{children:[e.jsx("td",{className:"muted nowrap",children:P(c.occurredAt)}),e.jsx("td",{children:c.username||e.jsx(M,{tone:"warn",children:c.userId||"unknown"})}),e.jsx("td",{children:c.query})]},`${c.occurredAt}:${l}`))})]})})})]})]})}function Fs(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 wi(){const{data:s,error:n,loading:t}=Q("/admin/api/views",{pollMs:6e4}),i=(s==null?void 0:s.daily)??[],r=(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(V,{message:n}),t?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[e.jsx(le,{tiles:[{label:Fs((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:Fs((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(T,{title:"Visits by day",intro:"One visit is a signed-in home-screen opening. Viewers are distinct household profiles.",icon:"chart",tone:"data",children:e.jsx(Z,{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(ae,{columns:3,children:"No home-screen visits yet."}):i.map(o=>e.jsxs("tr",{children:[e.jsx("td",{children:o.label}),e.jsx("td",{className:"num",children:w(o.visits)}),e.jsx("td",{className:"num",children:w(o.viewers)})]},o.label))})]})})}),e.jsx(T,{title:"Today by hour",intro:"Local New Zealand time. Use this to see when the household is opening Memby.",icon:"clock",tone:"info",children:e.jsx(Z,{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:r.length===0?e.jsx(ae,{columns:3,children:"No home-screen visits yet today."}):r.map(o=>e.jsxs("tr",{children:[e.jsx("td",{children:o.label}),e.jsx("td",{className:"num",children:w(o.visits)}),e.jsx("td",{className:"num",children:w(o.viewers)})]},o.label))})]})})})]})]})}const ki=s=>s.mediaType==="episode"?`${s.seriesTitle} S${String(s.seasonNumber).padStart(2,"0")}E${String(s.episodeNumber).padStart(2,"0")}`:s.title;function Ni(){var r;const s=Q("/admin/api/media-reports",{pollMs:15e3}),{busy:n,run:t}=X(),i=(o,a)=>t(`${o.id}-${a}`,async()=>{await F.post(`/admin/api/media-reports/${o.id}/status`,{status:a}),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(W,{rows:4}):e.jsx(T,{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:(r=s.data)!=null&&r.reports.length?e.jsx(Z,{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(o=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:ki(o)}),e.jsx("br",{}),e.jsx("span",{className:"muted",children:o.title})]}),e.jsxs("td",{children:[o.reason.replaceAll("_"," "),o.comment?e.jsxs(e.Fragment,{children:[e.jsx("br",{}),e.jsx("span",{className:"muted",children:o.comment})]}):null]}),e.jsxs("td",{children:[o.reportedByUsername,e.jsx("br",{}),e.jsx("span",{className:"muted",children:o.reportedByDevice||"Unknown device"})]}),e.jsx("td",{children:e.jsx(M,{tone:o.replacementRequested?"note":void 0,children:o.replacementRequested?o.replacementStatus||"Requested":"Not requested"})}),e.jsx("td",{children:e.jsx(M,{tone:o.status==="resolved"?"ok":o.status==="dismissed"?void 0:"warn",children:o.status})}),e.jsx("td",{className:"nowrap muted",children:P(o.createdAt)}),e.jsxs("td",{children:[e.jsx($,{size:"sm",variant:"quiet",busy:n===`${o.id}-acknowledged`,onClick:()=>void i(o,"acknowledged"),children:"Acknowledge"})," ",e.jsx($,{size:"sm",variant:"quiet",busy:n===`${o.id}-resolved`,onClick:()=>void i(o,"resolved"),children:"Resolve"})," ",e.jsx($,{size:"sm",variant:"quiet",busy:n===`${o.id}-dismissed`,onClick:()=>void i(o,"dismissed"),children:"Dismiss"})]})]},o.id))})]})}):e.jsx(Y,{children:"No media problems have been reported."})})]})}const Si=s=>s==="detected"?"ok":s==="failed"?"bad":s==="no_match"?"warn":"info",Ci=s=>s==="no_match"?"no match":s,Ps=s=>({"live-playback":"Live playback","tracearr-next":"Next episode","tracearr-binge-prefetch":"Binge look-ahead","multi-user-demand":"Multiple viewers"}[s]??s)||"Unknown",Os=(s,n)=>s>0&&n>0?`S${String(s).padStart(2,"0")}E${String(n).padStart(2,"0")}`:"Episode";function Mi(){var h,j,b,k;const s=Q("/admin/api/credits?limit=150",{pollMs:15e3}),{wrap:n}=ne(),{busy:t,run:i}=X(),[r,o]=p.useState(),[a,d]=p.useState(!1);p.useEffect(()=>{!a&&s.data&&o(s.data.settings)},[s.data,a]);const c=x=>{o(y=>y&&{...y,...x}),d(!0)},l=()=>{r&&i("save",async()=>{const x=await n(()=>F.put("/admin/api/credits",r),"Credits scanning settings saved.");x&&(s.set(x),o(x.settings),d(!1))})},v=((h=s.data)==null?void 0:h.history)??[],g=((j=s.data)==null?void 0:j.pending)??[],u=v.filter(x=>x.outcome==="detected").length,m=v.filter(x=>x.outcome==="no_match").length,f=v.filter(x=>x.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(V,{message:s.error}),s.loading||!r?e.jsx(W,{}):e.jsxs(e.Fragment,{children:[(b=s.data)!=null&&b.enabled?null:e.jsx(je,{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(le,{tiles:[{label:"Waiting candidates",value:w((k=s.data)==null?void 0:k.queueDepth),icon:"clock",tone:g.length?"info":void 0},{label:"Detected in this history",value:w(u),icon:"check",tone:"ok"},{label:"No match",value:w(m),icon:"search",tone:m?"warn":void 0},{label:"Failed",value:w(f),icon:"alert",tone:f?"bad":void 0}]}),e.jsxs(he,{cols:"wide",children:[e.jsx(T,{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:!a,onClick:l,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:r.candidateLimit,onChange:x=>c({candidateLimit:Number(x.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:r.prefetchEpisodes,onChange:x=>c({prefetchEpisodes:Number(x.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:r.prefetchEpisodes,max:20,value:r.maxPrefetch,onChange:x=>c({maxPrefetch:Number(x.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:r.retryHours,onChange:x=>c({retryHours:Number(x.target.value)})})})]})}),e.jsxs(T,{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(T,{title:"Waiting candidates",intro:"The exact worker order after marker checks and retry cooldowns. A refresh may replace this list as household viewing changes.",icon:"list",tone:"info",children:e.jsx(Z,{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:g.length===0?e.jsx(ae,{columns:6,children:"No episodes are waiting to be scanned."}):g.map(x=>e.jsxs("tr",{children:[e.jsx("td",{children:Os(x.season,x.episode)}),e.jsx("td",{children:e.jsx(M,{tone:"info",children:Ps(x.reason)})}),e.jsx("td",{className:"num",children:w(x.priority)}),e.jsx("td",{className:"num",children:w(x.userCount)}),e.jsx("td",{className:"nowrap muted",title:P(x.lastViewed),children:be(x.lastViewed)}),e.jsx("td",{className:"mono muted",children:x.itemId})]},x.itemId))})]})})}),e.jsx(T,{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(Z,{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:v.length===0?e.jsx(ae,{columns:7,children:"No credits scans have completed yet."}):v.map(x=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(x.finishedAt),children:be(x.finishedAt)}),e.jsxs("td",{children:[e.jsx("b",{children:x.seriesName||x.itemName||x.itemId}),e.jsxs("span",{className:"table-sub",children:[Os(x.season,x.episode),x.itemName&&x.seriesName?` · ${x.itemName}`:""]})]}),e.jsxs("td",{children:[e.jsx(M,{tone:"info",children:Ps(x.reason)}),e.jsxs("span",{className:"table-sub",children:["priority ",x.priority]})]}),e.jsxs("td",{children:[e.jsx(M,{tone:Si(x.outcome),children:Ci(x.outcome)}),x.error?e.jsx("span",{className:"table-sub",children:x.error}):null]}),e.jsx("td",{className:"nowrap",children:x.markerMs>0?Ne(x.markerMs):"—"}),e.jsxs("td",{className:"muted",children:[x.method||"visual",x.confidence>0?` · ${Ve(x.confidence)}`:"",x.frames>0?` · ${x.frames} frames`:""]}),e.jsx("td",{className:"num muted",children:Ne(x.durationMs)})]},x.id))})]})})})]})]})}function Ei(){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 Ai(){return e.jsx(hn,{children:e.jsx(Cn,{children:e.jsx($n,{children:e.jsx(Fn,{children:e.jsx(un,{children:e.jsxs(B,{path:"/admin",element:e.jsx(qn,{}),children:[e.jsx(B,{index:!0,element:e.jsx(Pn,{})}),e.jsx(B,{path:"activity",element:e.jsx(_n,{})}),e.jsx(B,{path:"accounts",element:e.jsx(Un,{})}),e.jsx(B,{path:"accounts/:userId",element:e.jsx(Vn,{})}),e.jsx(B,{path:"accounts/:userId/settings",element:e.jsx(Gn,{})}),e.jsx(B,{path:"clients",element:e.jsx(Jn,{})}),e.jsx(B,{path:"logins",element:e.jsx(Qn,{})}),e.jsx(B,{path:"devices/:deviceId",element:e.jsx(nt,{})}),e.jsx(B,{path:"library",element:e.jsx(tt,{})}),e.jsx(B,{path:"ratings",element:e.jsx(at,{})}),e.jsx(B,{path:"requests",element:e.jsx(rt,{})}),e.jsx(B,{path:"recommendations",element:e.jsx(lt,{})}),e.jsx(B,{path:"inspector",element:e.jsx(dt,{})}),e.jsx(B,{path:"hero",element:e.jsx(pt,{})}),e.jsx(B,{path:"features",element:e.jsx(jt,{})}),e.jsx(B,{path:"playback",element:e.jsx(vt,{})}),e.jsx(B,{path:"subtitles",element:e.jsx(gt,{})}),e.jsx(B,{path:"credits",element:e.jsx(Mi,{})}),e.jsx(B,{path:"updates",element:e.jsx(bt,{})}),e.jsx(B,{path:"tasks",element:e.jsx(Nt,{})}),e.jsx(B,{path:"integrations",element:e.jsx(Ct,{})}),e.jsx(B,{path:"maintenance",element:e.jsx(Tt,{})}),e.jsx(B,{path:"settings",element:e.jsx(Lt,{})}),e.jsx(B,{path:"imports",element:e.jsx(qt,{})}),e.jsx(B,{path:"logs",element:e.jsx(mi,{})}),e.jsx(B,{path:"journeys",element:e.jsx(pi,{})}),e.jsx(B,{path:"journeys/:userId",element:e.jsx(bi,{})}),e.jsx(B,{path:"views",element:e.jsx(wi,{})}),e.jsx(B,{path:"engagement",element:e.jsx(fi,{})}),e.jsx(B,{path:"searches",element:e.jsx(yi,{})}),e.jsx(B,{path:"media-reports",element:e.jsx(Ni,{})}),e.jsx(B,{path:"overview",element:e.jsx(mn,{to:"/admin",replace:!0})}),e.jsx(B,{path:"*",element:e.jsx(Ei,{})})]})})})})})})}const rn=document.getElementById("root");if(!rn)throw new Error("the console has no root element to render into");Bs(rn).render(e.jsx(p.StrictMode,{children:e.jsx(Ai,{})})); diff --git a/admin-ui/dist/index.html b/admin-ui/dist/index.html index 0898f4e..87e2e0f 100644 --- a/admin-ui/dist/index.html +++ b/admin-ui/dist/index.html @@ -13,9 +13,9 @@ 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/lib/format.ts b/admin-ui/src/lib/format.ts index 4ae6be6..e7d61b9 100644 --- a/admin-ui/src/lib/format.ts +++ b/admin-ui/src/lib/format.ts @@ -25,6 +25,25 @@ export function duration(ms: number | undefined | null): string { return `${Math.floor(minutes / 60)}h ${minutes % 60}m`; } +/** watchTime words a span of viewing, where `duration` words a span of machine time. + * + * Two formatters rather than one because they answer different questions: a request that + * took 1400ms wants its milliseconds, and an evening in front of the television does not — + * it is measured in hours and minutes and rounds to the minute. Nothing under a minute is a + * figure at all, and zero says so in words rather than printing "0m", which reads as a + * reading that failed rather than as an evening off. The wording deliberately matches the + * gateway's own formatWatchDuration, so the console and the summary a viewer receives + * cannot describe the same week two ways. */ +export function watchTime(ms: number | undefined | null): string { + const minutes = Math.round(Math.max(0, ms ?? 0) / 60000); + if (minutes <= 0) return 'none'; + if (minutes < 60) return `${minutes} min`; + const hours = Math.floor(minutes / 60); + const rest = minutes % 60; + if (rest === 0) return hours === 1 ? '1 hour' : `${hours} hours`; + return `${hours}h ${rest}m`; +} + /** interval describes a schedule, where "3600s" is a worse answer than "every hour". */ export function interval(seconds: number | undefined | null): string { if (!seconds || seconds <= 0) return 'on request only'; @@ -84,12 +103,17 @@ const IDLE_MS = 3 * 60 * 60 * 1000; export const recent = (value: string | undefined | null): boolean => Boolean(value) && Date.now() - new Date(value as string).getTime() < ACTIVE_MS; -export type Tone = 'ok' | 'warn' | 'bad' | 'info' | 'note' | 'data'; +export type Tone = 'ok' | 'idle' | 'warn' | 'bad' | 'info' | 'note' | 'data'; /* Three states rather than two, because "not active this minute" covers both a set somebody switched off after breakfast and one that has not been seen since lunchtime — - and only the second is worth an operator's attention. Green is on now, amber is a set - in ordinary use that happens to be off, and red is one that has stopped checking in for + and only the second is worth an operator's attention. + The middle state is the quiet green rather than amber. Amber is the console's "look at + this", and a television that checked in an hour ago is the ordinary condition of every + set in a house at any given moment: a page of amber dots every evening is a page that + teaches an operator to ignore the colour, which is exactly what it must not do on the + evening one of them really has stopped. Two shades of green say what is true — both are + fine, one is connected right now — and red is kept for a set that has not been seen for three hours. A device with no timestamp at all is red: never seen is the strongest version of not seen. */ export function presence(value: string | undefined | null): { tone: Tone; label: string } { @@ -97,7 +121,7 @@ export function presence(value: string | undefined | null): { tone: Tone; label: if (!seen) return { tone: 'bad', label: 'never seen' }; const age = Date.now() - seen; if (age < ACTIVE_MS) return { tone: 'ok', label: 'active now' }; - if (age < IDLE_MS) return { tone: 'warn', label: 'seen recently' }; + if (age < IDLE_MS) return { tone: 'idle', label: 'seen recently' }; return { tone: 'bad', label: 'not seen lately' }; } diff --git a/admin-ui/src/lib/logmodel.ts b/admin-ui/src/lib/logmodel.ts new file mode 100644 index 0000000..a39379d --- /dev/null +++ b/admin-ui/src/lib/logmodel.ts @@ -0,0 +1,570 @@ +import type { LogEvent } from '../api/types'; + +/* The shape of a server event, for a table rather than for a file. + * + * The gateway writes structured records — a message and a bag of attributes — and the old + * page rendered the bag as `component=admin client=unknown method=GET path=… status=200`, + * which is every fact and no hierarchy. A person reading a log is asking four questions in + * order: when, which part of the server, what happened, did it work. This module answers + * them, so the table can print the answers and the drawer can keep the evidence. + * + * It is deliberately *client*-side and derives everything from attributes the gateway + * already sends. The ring buffer is restored from an archive across deployments, so a + * record written by yesterday's build is in the window beside one written a minute ago; + * a shaping rule that lived on the server would render the older half as raw text for as + * long as the archive holds it. Nothing here asks the API for a field it does not have. + * + * Everything below is pure. `shape` is memoised on the event object itself, which is what + * lets the table filter and re-window twenty thousand records without re-deriving them. */ + +export type LogTone = 'ok' | 'warn' | 'bad' | 'info' | 'note' | 'data' | 'idle' | 'quiet'; + +export interface Shaped { + /** Stable identity for a service badge — also the value the service filter matches on. */ + serviceKey: string; + service: string; + component: string; + /** The verb: an HTTP method, or a short word for an application event. */ + action: string; + /** What happened, in words: "GET Notifications", "Started Blue Bloods · S04E08". */ + summary: string; + /** Who or what it was for. Medium weight, beside the summary. */ + context: string; + /** The one line that explains a failure, printed under the row rather than hidden. */ + detail: string; + result: { label: string; short: string; tone: LogTone } | null; + durationMs: number | null; + method: string; + status: number | null; + /** The raw message, which is what the Event filter matches on. */ + eventKey: string; + level: string; + time: string; + day: string; + dayKey: string; + /** Whether the row earns a second line. */ + tall: boolean; + haystack: string; + fields: [string, unknown][]; + attributes: Record; +} + +/* ---------- services ---------- */ + +/* A subsystem's identity has to be the same every time it appears or there is nothing to + * recognise. The label is the identity; the tone is a secondary cue and is drawn from the + * console's existing secondary palette, never from the verdict colours — green, amber and + * red mean good, look and wrong on every other page and must not start meaning "playback" + * here. Related subsystems share a tone on purpose: five hues that mean something are + * worth more than a dozen that only mean "different". */ +const SERVICE_TONE: Record = { + 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', +}; + +export const serviceTone = (key: string): LogTone => SERVICE_TONE[key] ?? 'quiet'; + +/* The component attribute is derived from the route by the gateway, so it is one flat + * token — `admin`, `playback`, `details`. This is where it becomes a place in the server: + * a service worth recognising and the part of it that spoke. */ +const PLACES: Record = { + 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 message can be more specific than the route it arrived on. A subtitle search is a + * subtitle event whichever route asked for it, and an upstream failure belongs to Emby + * rather than to the screen that was unlucky enough to be waiting on it. These run after + * the route lookup and only ever narrow it. */ +const REFINEMENTS: [RegExp, [string, string, string]][] = [ + [/^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 placeFor(component: string, message: string): [string, string, string] { + const lower = message.toLowerCase(); + for (const [pattern, place] of REFINEMENTS) { + if (pattern.test(lower)) return place; + } + const known = PLACES[component]; + if (known) return known; + if (!component) return ['gateway', 'Gateway', 'Server']; + return ['gateway', 'Gateway', titleCase(component.replace(/[-_]/g, ' '))]; +} + +/* ---------- durations ---------- */ + +const UNIT_MS: Record = { + h: 3_600_000, + m: 60_000, + s: 1000, + ms: 1, + us: 0.001, + 'µs': 0.001, + ns: 0.000001, +}; + +/** Go prints a duration as `2ms`, `1.482s`, `1m30s`, `418µs`. Sum whatever it wrote. */ +export function parseDuration(value: unknown): number | null { + if (typeof value === 'number') return Number.isFinite(value) ? value : null; + if (typeof value !== 'string' || !value) return null; + const matches = value.matchAll(/([0-9]*\.?[0-9]+)(ns|µs|us|ms|h|m|s)/g); + let total = 0; + let seen = false; + for (const match of matches) { + const unit = match[2] ? UNIT_MS[match[2]] : undefined; + if (unit === undefined) continue; + total += Number(match[1]) * unit; + seen = true; + } + return seen ? total : null; +} + +export function formatDuration(ms: number): string { + if (ms < 1) return '<1 ms'; + if (ms < 1000) return `${Math.round(ms)} ms`; + if (ms < 10_000) return `${(ms / 1000).toFixed(1)} s`; + if (ms < 60_000) return `${Math.round(ms / 1000)} s`; + const minutes = Math.floor(ms / 60_000); + return `${minutes}m ${Math.round((ms % 60_000) / 1000)}s`; +} + +/* Two thresholds and no more. The point of tinting a duration is that a slow row can be + * found by eye down a column of hundreds; a gradient over every row would just be a + * second colour scheme nobody can read a value out of. */ +export const durationTone = (ms: number): LogTone | null => + ms >= 3000 ? 'bad' : ms >= 1000 ? 'warn' : null; + +/* ---------- HTTP ---------- */ + +const STATUS_TEXT: Record = { + 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', +}; + +export function statusText(code: number): string { + const known = STATUS_TEXT[code]; + if (known) return known; + if (code >= 500) return 'Server Error'; + if (code >= 400) return 'Client Error'; + if (code >= 300) return 'Redirected'; + if (code >= 200) return 'OK'; + return 'Response'; +} + +const statusTone = (code: number): LogTone => + code >= 500 ? 'bad' : code >= 400 ? 'warn' : code >= 300 ? 'quiet' : 'ok'; + +/* A few routes are named things rather than paths, and every one of them is high traffic: + * the health probe, the status poll and artwork are most of what a busy log contains, so + * they are the ones worth reading as words. */ +const NAMED_PATHS: Record = { + '/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', +}; + +/* An id is a segment with a number in it that is not a word — every Emby id, every user id + * and every frame number, and none of `playback`, `trickplay` or `notifications`. Route + * segments in this gateway are lower-case words, so "contains a digit" is a sound test and + * a wrong answer costs a word in a summary rather than anything an operator acts on. */ +const idLike = (segment: string) => /\d/.test(segment) || segment.length > 24; + +/** `/admin/api/notifications` reads as "Notifications"; the path itself stays in the + * drawer, the tooltip and the export. An id is dropped rather than printed — nobody can + * read one, and it is the thing that makes two rows of the same route look different. */ +export function prettyPath(path: string): string { + const named = NAMED_PATHS[path]; + if (named) return named; + const segments = path.split('/').filter((part) => part && part !== 'v1' && part !== 'api'); + if (segments[0] === 'admin') segments.shift(); + const words = segments.filter((part) => !idLike(part)); + if (words.length === 0) return path; + return titleCase(words.join(' ').replace(/[-_.]/g, ' ').replace(/\s+/g, ' ').trim()); +} + +/* ---------- wording ---------- */ + +const titleCase = (value: string) => + value ? value.charAt(0).toUpperCase() + value.slice(1) : value; + +const sentence = (message: string) => titleCase(message.replace(/_/g, ' ')); + +const text = (value: unknown): string => + value === undefined || value === null ? '' : String(value); + +/* `client=unknown` and `protocol=unknown` are what the gateway writes when a request did + * not say, which is most of them. They are facts and they belong in the export; they are + * not information and they must not take a column. */ +const KNOWN_NOTHING = new Set(['', 'unknown', 'none', 'null', '', '0']); +const informative = (value: unknown) => !KNOWN_NOTHING.has(text(value).toLowerCase()); + +/* The one attribute that says what the row is *about*, in the order a person would look + * for it. A title beats an id every time, which is the whole reason the gateway logs one. */ +const SUBJECT_KEYS = ['title', 'series', 'name', 'query', 'item_title', 'file']; + +/* Non-HTTP outcomes worth a result chip. The key is the attribute; the tone is the + * verdict. Anything not listed simply has no result rather than an invented one. */ +const PLAY_METHOD_TONE: Record = { + directplay: 'ok', + direct: 'ok', + directstream: 'ok', + transcode: 'warn', + transcoding: 'warn', +}; + +function resultFor( + attributes: Record, + level: string, + status: number | null, +): Shaped['result'] { + if (status !== null) { + return { label: `${status} ${statusText(status)}`, short: String(status), tone: statusTone(status) }; + } + if (informative(attributes.error)) { + return { label: 'Failed', short: 'Failed', tone: level === 'WARN' ? 'warn' : 'bad' }; + } + const method = text(attributes.play_method).toLowerCase().replace(/[\s_-]/g, ''); + if (method && !KNOWN_NOTHING.has(method)) { + // Emby writes `DirectPlay`; a person reads "Direct Play". + const label = titleCase(text(attributes.play_method).replace(/([a-z])([A-Z])/g, '$1 $2')); + return { label, short: label, tone: PLAY_METHOD_TONE[method] ?? 'info' }; + } + if (informative(attributes.cache)) { + const hit = /hit|true|yes/i.test(text(attributes.cache)); + return { + label: hit ? 'Cached' : 'Cache miss', + short: hit ? 'Cached' : 'Miss', + tone: hit ? 'data' : 'quiet', + }; + } + if (level === 'ERROR') return { label: 'Failed', short: 'Failed', tone: 'bad' }; + if (level === 'WARN') return { label: 'Warning', short: 'Warning', tone: 'warn' }; + return null; +} + +/* ---------- context line ---------- */ + +/* Who it was for, and the one or two facts that make an application event mean something. + * Kept short on purpose: this sits beside the summary, and a context line that wraps has + * stopped being context. */ +function contextFor(attributes: Record): string { + const parts: string[] = []; + if (informative(attributes.user)) parts.push(text(attributes.user)); + if (informative(attributes.device)) parts.push(text(attributes.device)); + const marker = parseDuration(attributes.marker_ms); + if (marker !== null && marker > 0) parts.push(`Start ${clock(marker)}`); + const position = parseDuration(attributes.position); + if (position !== null && position > 0) parts.push(`At ${clock(position)}`); + if (informative(attributes.watched)) parts.push(`${text(attributes.watched)} watched`); + if (informative(attributes.reason)) parts.push(text(attributes.reason)); + return parts.slice(0, 3).join(' · '); +} + +/** A position inside a programme is read as a time, not as a number of milliseconds. */ +function clock(ms: number): string { + const total = Math.round(ms / 1000); + const hours = Math.floor(total / 3600); + const minutes = Math.floor((total % 3600) / 60); + const seconds = total % 60; + const pad = (value: number) => String(value).padStart(2, '0'); + return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${minutes}:${pad(seconds)}`; +} + +/* ---------- field grouping for the drawer ---------- */ + +/* The drawer answers "exactly how and why", and it answers it in sections rather than as + * one alphabetical dump. A key that belongs to no section still appears — under Details — + * because a record must never be able to hide a field from the person reading it. */ +export const DRAWER_SECTIONS: { title: string; keys: string[] }[] = [ + { 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: 'Diagnostics', keys: ['error', 'stack', 'correlation', 'version', 'gateway_version', 'duration'] }, +]; + +const SECTIONED = new Set(DRAWER_SECTIONS.flatMap((section) => section.keys)); +export const isSectioned = (key: string) => SECTIONED.has(key); + +/* ---------- shaping ---------- */ + +const timeFormat = new Intl.DateTimeFormat('en-NZ', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, +}); +const dayFormat = new Intl.DateTimeFormat('en-NZ', { + weekday: 'short', + day: 'numeric', + month: 'short', +}); + +// Records are immutable for their retained lifetime, so a WeakMap gives the derived view +// exactly that lifetime without adding private fields to anything the export writes out. +const cache = new WeakMap(); + +export function shape(event: LogEvent): Shaped { + const existing = cache.get(event); + if (existing) return existing; + const value = derive(event); + cache.set(event, value); + return value; +} + +function derive(event: LogEvent): Shaped { + const attributes = event.attributes ?? {}; + const message = event.message ?? ''; + const [serviceKey, service, component] = placeFor(text(attributes.component), message); + + const path = text(attributes.path); + // `method` is only an HTTP method when there is a route beside it: the credits scanner + // logs a detection method under the same key, and a filter offering VISUAL beside GET + // would be two different questions sharing a control. + const method = path ? text(attributes.method).toUpperCase() : ''; + const statusRaw = Number(attributes.status); + const status = path && Number.isFinite(statusRaw) && statusRaw > 0 ? statusRaw : null; + const isRequest = message === 'request' && Boolean(path); + + const subjectKey = SUBJECT_KEYS.find((key) => informative(attributes[key])); + const subject = subjectKey ? text(attributes[subjectKey]) : ''; + + let action: string; + let summary: string; + if (isRequest) { + action = method || 'HTTP'; + summary = prettyPath(path); + } else if (path && method) { + action = method; + summary = subject ? `${sentence(message)} · ${subject}` : `${sentence(message)} — ${prettyPath(path)}`; + } else { + action = ''; + summary = subject ? `${sentence(message)} · ${subject}` : sentence(message); + } + + const durationMs = parseDuration( + attributes.duration ?? attributes.duration_ms ?? attributes.negotiation_duration, + ); + const detail = informative(attributes.error) ? text(attributes.error) : ''; + const context = contextFor(attributes); + const occurred = new Date(event.occurredAt); + + const fields = Object.entries(attributes); + const shaped: Shaped = { + serviceKey, + service, + component, + action, + summary, + context, + detail, + result: resultFor(attributes, event.level, status), + durationMs, + method, + status, + eventKey: message, + level: event.level, + time: `${timeFormat.format(occurred)}.${String(occurred.getMilliseconds()).padStart(3, '0')}`, + day: dayFormat.format(occurred), + dayKey: occurred.toDateString(), + // An error explains itself on a second line; so does an application event carrying a + // person or a position. Ordinary request traffic — which is most of a log — stays on + // one, because density is the whole reason this page is worth watching. + tall: Boolean(detail) || (Boolean(context) && !isRequest), + haystack: [ + message, service, component, summary, context, detail, + ...fields.flat().map(text), + ] + .join(' ') + .toLowerCase(), + fields, + attributes, + }; + return shaped; +} + +/* ---------- filtering ---------- */ + +export const LEVEL_RANK: Record = { TRACE: 5, DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 }; + +export interface LogFilters { + level: string; + service: string; + component: string; + event: string; + method: string; + /** '' | '2xx' | '3xx' | '4xx' | '5xx' | 'error' (anything at or above 400). */ + status: string; + /** Minimum duration in milliseconds; 0 means no duration filter. */ + slower: number; + text: string; +} + +export const EMPTY_FILTERS: LogFilters = { + level: 'INFO', + service: '', + component: '', + event: '', + method: '', + status: '', + slower: 0, + text: '', +}; + +function statusMatches(rule: string, status: number | null): boolean { + if (!rule) return true; + if (status === null) return false; + if (rule === 'error') return status >= 400; + const band = Number(rule[0]); + return Math.floor(status / 100) === band; +} + +export function matches(event: LogEvent, filters: LogFilters, search: string): boolean { + if ((LEVEL_RANK[event.level] ?? 0) < (LEVEL_RANK[filters.level] ?? 20)) return false; + const view = shape(event); + if (filters.service && view.serviceKey !== filters.service) return false; + if (filters.component && view.component !== filters.component) return false; + if (filters.event && view.eventKey !== filters.event) return false; + if (filters.method && view.method !== filters.method) return false; + if (!statusMatches(filters.status, view.status)) return false; + if (filters.slower > 0 && (view.durationMs ?? 0) < filters.slower) return false; + if (search && !view.haystack.includes(search)) return false; + return true; +} + +/** The facets actually present in what has been retained. Offering a service nothing has + * logged is a filter that can only ever empty the table. */ +export function facets(records: LogEvent[]): { + services: { key: string; label: string }[]; + components: string[]; + events: string[]; + methods: string[]; +} { + const services = new Map(); + const components = new Set(); + const events = new Set(); + const methods = new Set(); + for (const record of records) { + const view = shape(record); + services.set(view.serviceKey, view.service); + components.add(view.component); + events.add(view.eventKey); + if (view.method) methods.add(view.method); + } + return { + services: [...services].map(([key, label]) => ({ key, label })).sort((a, b) => a.label.localeCompare(b.label)), + components: [...components].sort((a, b) => a.localeCompare(b)), + events: [...events].sort((a, b) => a.localeCompare(b)), + methods: [...methods].sort((a, b) => a.localeCompare(b)), + }; +} + +/** The chips shown above the table: one per narrowing in force, each removable. */ +export function activeChips( + filters: LogFilters, + services: { key: string; label: string }[], +): { key: keyof LogFilters; label: string }[] { + const chips: { key: keyof LogFilters; label: string }[] = []; + if (filters.service) { + const label = services.find((entry) => entry.key === filters.service)?.label ?? filters.service; + chips.push({ key: 'service', label: `Service: ${label}` }); + } + if (filters.component) chips.push({ key: 'component', label: `Component: ${filters.component}` }); + if (filters.event) chips.push({ key: 'event', label: `Event: ${filters.event}` }); + if (filters.method) chips.push({ key: 'method', label: `Method: ${filters.method}` }); + if (filters.status) { + chips.push({ + key: 'status', + label: `Status: ${filters.status === 'error' ? '≥400' : filters.status}`, + }); + } + if (filters.slower > 0) { + chips.push({ key: 'slower', label: `Duration: >${formatDuration(filters.slower)}` }); + } + if (filters.text) chips.push({ key: 'text', label: `Search: ${filters.text}` }); + return chips; +} diff --git a/admin-ui/src/pages/Account.tsx b/admin-ui/src/pages/Account.tsx index 3b983ac..2176d42 100644 --- a/admin-ui/src/pages/Account.tsx +++ b/admin-ui/src/pages/Account.tsx @@ -3,7 +3,7 @@ import { Link, useNavigate, useParams } from 'react-router-dom'; import { api } from '../api/client'; import { useAction, useQuery } from '../lib/hooks'; import { useToast } from '../lib/toast'; -import { initials, num, presence, recent, when } from '../lib/format'; +import { initials, num, presence, recent, watchTime, when } from '../lib/format'; import { Banner, Button, @@ -16,6 +16,7 @@ import { Loading, PageHead, Tag, + Tiles, Toggle, } from '../components/ui'; import type { DeviceVersion } from '../api/types'; @@ -78,9 +79,24 @@ interface NotificationPreferences { updateAlerts: boolean; libraryAlerts: boolean; systemAlerts: boolean; + watchTimeDigest: boolean; leadDays: number; } +/* Tracearr's reading of this person. `matched` separates "no Tracearr, or nobody by this + name in it" from "has watched nothing", which are the same row of zeroes on the wire and + very different things for an operator to be told. */ +interface WatchTime { + matched: boolean; + tracearrUsername?: string; + weekMs: number; + monthMs: number; + totalMs: number; + weekSessions: number; + monthSessions: number; + lastWatchedAt?: string; +} + interface AccountDetail { id: string; username: string; @@ -97,6 +113,7 @@ interface AccountDetail { preferences?: Record; }; notifications: NotificationPreferences; + watchTime?: WatchTime; } interface AccountsPayload { @@ -338,6 +355,53 @@ export function AccountPage() { + {/* Read from Tracearr and shown only when Tracearr has an answer. The card is absent + rather than empty for a household running none: a permanently blank panel on every + account page teaches an operator to scroll past that part of the screen. */} + {account.watchTime?.matched ? ( + {account.watchTime.tracearrUsername} + ) : null + } + > + + + ) : null} + current && { ...current, libraryAlerts }) } /> + + setNotifications((current) => current && { ...current, watchTimeDigest }) + } + /> account.recommendations?.prompted && !account.recommendations?.completed, ).length; + /* Summed from the same rows the list draws, so the tile and the column underneath it can + never disagree — a separate total query is how those two come apart. */ + const tracked = accounts.filter((account) => account.watchTime?.matched); + const weekMs = tracked.reduce((total, account) => total + (account.watchTime?.weekMs ?? 0), 0); return ( <> @@ -60,6 +80,16 @@ export function AccountsPage() { }, { label: 'recommendation setups completed', value: num(completed), icon: 'check', tone: 'ok' }, { label: 'setup prompts queued', value: num(queued), icon: 'sparkle', tone: 'note' }, + ...(tracked.length + ? [ + { + label: 'watched by the household this week', + value: watchTime(weekMs), + icon: 'pulse' as const, + tone: 'data' as const, + }, + ] + : []), ]} /> @@ -78,6 +108,7 @@ export function AccountsPage() { ? { label: 'prompt queued', tone: 'warn' as const } : { label: 'not invited', tone: undefined }; const seen = presence(account.lastSeen); + const watched = account.watchTime; return ( @@ -90,10 +121,17 @@ export function AccountsPage() { {num(list.length)} device{list.length === 1 ? '' : 's'} {active ? ` · ${active} active now` : ''} · last seen {when(account.lastSeen)} + {/* The month sits beside the week because a quiet week only means + something next to the month around it. Both are omitted rather + than zeroed for somebody Tracearr has never seen. */} + {watched?.matched + ? ` · watched ${watchTime(watched.weekMs)} this week, ${watchTime(watched.monthMs)} this month` + : ''} + {watched?.matched ? {watchTime(watched.weekMs)} : null} {state.label} Manage diff --git a/admin-ui/src/pages/JourneyViewer.tsx b/admin-ui/src/pages/JourneyViewer.tsx index dd353b4..2d6fe95 100644 --- a/admin-ui/src/pages/JourneyViewer.tsx +++ b/admin-ui/src/pages/JourneyViewer.tsx @@ -16,8 +16,10 @@ interface JourneyEvent { feature?: string; source?: string; target?: string; + itemId?: string; itemName?: string; itemType?: string; + playSessionId?: string; outcome?: string; } @@ -32,17 +34,42 @@ const label = (value: string | undefined | null) => { }; const place = (event: JourneyEvent | undefined) => label(event?.target || event?.screen || event?.source || event?.feature); + +/* Where a viewing journey began. + * + * The journey is cut at the playback request, so the first event's `target` is "player" for + * every one of them — which is why journeys used to read as somebody appearing in the player + * from nowhere. The request's `source` is the entry point the television stated + * (continue_watching, magic_movie, ...), and it is the answer to this question; the first + * event's own place is only the fallback for a journey that never reached playback. */ +const entryPoint = (events: JourneyEvent[]) => { + const request = events.find((event) => event.category === 'playback' && event.action === 'request'); + return request?.source ? label(request.source) : place(events[0]); +}; const detail = (event: JourneyEvent) => event.itemName ? `${label(event.itemType)} · ${event.itemName}` : event.source && event.target ? `${label(event.source)} → ${label(event.target)}` : place(event); const verb = (event: JourneyEvent) => ({ journey_start: 'Opened Memby', home_open: 'Opened Memby', journey_end: 'Finished session', screen_view: 'Viewed', select: 'Selected', open: 'Opened', close: 'Closed', - request: event.category === 'playback' ? 'Started watching' : 'Requested', - stop: 'Stopped watching', start: 'Started', complete: 'Completed', + request: event.category === 'playback' ? 'Asked to watch' : 'Requested', + stop: 'Left the player', + start: event.category === 'playback' + ? (event.outcome === 'failure' ? 'Playback failed' : 'Started watching') + : 'Started', + complete: event.category === 'playback' + ? (event.outcome === 'completed' ? 'Finished watching' : 'Stopped watching') + : 'Completed', }[event.action] ?? label(event.action)); function outcome(events: JourneyEvent[]) { + /* A playback step is the verdict on a viewing journey, so it outranks whatever incidental + * outcome a favourite toggle or a settings change left behind on the way in. */ + const playback = [...events].reverse().find((event) => event.category === 'playback' && event.outcome); + if (playback?.outcome === 'failure') return { label: 'playback failed', tone: 'warn' as const }; + if (playback?.outcome === 'completed') return { label: 'watched', tone: 'ok' as const }; + if (playback?.outcome === 'abandoned') return { label: 'stopped part-way', tone: 'note' as const }; + if (playback?.outcome === 'success') return { label: 'watched', tone: 'ok' as const }; const explicit = [...events].reverse().find((event) => event.outcome)?.outcome; if (explicit === 'success' || explicit === 'completed') return { label: label(explicit), tone: 'ok' as const }; if (explicit === 'failure' || explicit === 'cancelled' || explicit === 'abandoned') return { label: label(explicit), tone: 'note' as const }; @@ -91,7 +118,7 @@ export function JourneyViewerPage() { return
{when(entry?.occurredAt)}Journey {index + 1} · {events.length} recorded steps
{result.label}
-
Entered from{place(entry)}
+
Entered from{entryPoint(events)}
Selected{selection ? detail(selection) : 'Nothing selected'}
Outcome{result.label}
diff --git a/admin-ui/src/pages/Journeys.tsx b/admin-ui/src/pages/Journeys.tsx index f4fbc4e..abee12d 100644 --- a/admin-ui/src/pages/Journeys.tsx +++ b/admin-ui/src/pages/Journeys.tsx @@ -26,7 +26,8 @@ import { const FEATURE_CATALOGUE = [ 'home', 'movies', 'shows', 'favorites', 'search', 'recent_searches', 'genre_browse', 'for_you', 'for_you_time', 'recommendation', 'continue', - 'latest', 'my_shows', 'details', 'playback', 'notifications', 'profiles', 'settings', + 'latest', 'my_shows', 'details', 'playback', 'magic_movie', 'notifications', + 'profiles', 'settings', ]; /** The wire values are ids; this is the render, so it is spelled — the same boundary the diff --git a/admin-ui/src/pages/Logs.tsx b/admin-ui/src/pages/Logs.tsx index 1dbc143..0611bda 100644 --- a/admin-ui/src/pages/Logs.tsx +++ b/admin-ui/src/pages/Logs.tsx @@ -9,127 +9,384 @@ import { useRef, useState, } from 'react'; +import type { ReactNode } from 'react'; import { api } from '../api/client'; import { num } from '../lib/format'; -import { Banner, Button, Card, Field, Note, PageHead } from '../components/ui'; +import { Banner, Button, Card, Note, PageHead } from '../components/ui'; +import { Icon } from '../components/Icon'; +import { + DRAWER_SECTIONS, + EMPTY_FILTERS, + activeChips, + durationTone, + facets, + formatDuration, + isSectioned, + matches, + serviceTone, + shape, +} from '../lib/logmodel'; +import type { LogFilters, Shaped } from '../lib/logmodel'; import type { LogEvent, LogResponse } from '../api/types'; /* The live server log. * - * Network delivery is already cursor based: each server record crosses the wire once. - * Rendering is virtualised as well, so retaining and filtering thousands of records does - * not mean mounting thousands of details trees. Only the rows around the viewport exist - * in the DOM; selecting one opens its complete structured data below the window. */ + * Network delivery is cursor based: each server record crosses the wire once. Rendering is + * virtualised, so retaining and filtering thousands of records does not mean mounting + * thousands of rows — only those around the viewport exist in the DOM. + * + * What changed, and why: the table used to print a record's attribute bag as one string, + * which is complete and unreadable. `lib/logmodel` turns a record into the four answers a + * person is actually after — when, which part of the server, what happened, did it work — + * and this file is only the table, the filters and the drawer over that. The drawer is + * where the evidence lives now, rather than where the meaning was. + * + * Three properties are easy to give back and worth keeping: + * + * - **Rows have two heights, not one.** Repetitive request traffic stays on one line, + * because density is the reason this page is worth watching; a failure or a real + * application event earns a second. That means the virtual window is driven by a prefix + * sum of row heights rather than by multiplication, computed once per filter change. + * - **Pause holds the view, not the connection.** Draining continues while paused and the + * arrivals are held in a buffer, so the cursor keeps up with the server's ring buffer + * and resuming is a flush rather than a stampede — the previous behaviour let the ring + * overwrite records the operator had paused specifically in order to read around. + * - **Nothing snaps.** The view follows the tail only while the operator is already at + * it; scrolling up hands them a `Jump to latest` instead. */ -const RANKS: Record = { TRACE: 5, DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40 }; const RETAIN = 20_000; const POLL_MS = 5_000; -const ROW_HEIGHT = 48; +const ROW_COMPACT = 30; +const ROW_TALL = 48; +const DAY_HEIGHT = 26; const HEADER_HEIGHT = 31; -const OVERSCAN = 8; +const OVERSCAN = 10; -const FIELD_ORDER = [ - 'component', - 'user', - 'device', - 'client', - 'protocol', - 'method', - 'path', - 'status', - 'duration', - 'version', - 'gateway_version', +const LEVELS = [ + { value: 'TRACE', label: 'Everything' }, + { value: 'DEBUG', label: 'Debug+' }, + { value: 'INFO', label: 'Info+' }, + { value: 'WARN', label: 'Warnings+' }, + { value: 'ERROR', label: 'Errors only' }, ]; -const FIELD_RANK = new Map(FIELD_ORDER.map((key, index) => [key, index])); -const dateTime = new Intl.DateTimeFormat(undefined, { dateStyle: 'short', timeStyle: 'medium' }); -interface CachedEvent { - fields: [string, unknown][]; - summary: string; - haystack: string; - occurred: string; -} +const STATUS_BANDS = [ + { 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)' }, +]; -// API event objects remain stable for their retained lifetime. A WeakMap gives formatting -// and search indexing the same lifetime without adding private fields to JSON exports. -const eventCache = new WeakMap(); - -function cached(event: LogEvent): CachedEvent { - const existing = eventCache.get(event); - if (existing) return existing; - const fields = Object.entries(event.attributes ?? {}).sort((left, right) => { - const leftRank = FIELD_RANK.get(left[0]) ?? (left[0] === 'error' ? 1000 : 100); - const rightRank = FIELD_RANK.get(right[0]) ?? (right[0] === 'error' ? 1000 : 100); - return leftRank - rightRank || left[0].localeCompare(right[0]); - }); - const value = { - fields, - summary: fields.map(([key, fieldValue]) => `${key}=${String(fieldValue)}`).join(' '), - haystack: [event.message, ...fields.flat()].join(' ').toLowerCase(), - occurred: dateTime.format(new Date(event.occurredAt)), - }; - eventCache.set(event, value); - return value; -} +const SLOWER = [ + { value: 0, label: 'Any duration' }, + { value: 100, label: 'Slower than 100 ms' }, + { value: 500, label: 'Slower than 500 ms' }, + { value: 1000, label: 'Slower than 1 s' }, + { value: 3000, label: 'Slower than 3 s' }, +]; const readableKey = (key: string) => key.replace(/_/g, ' '); -const LogLine = memo(function LogLine({ +/* ---------- one row ---------- */ + +/** A value in the table that is also a filter. Clicking a service, a level, a method or a + * status is by some way the fastest way into a subsystem, and it costs nothing to make + * the thing already printed be the control. */ +function Facet({ + onPick, + className, + title, + children, + ...rest +}: { + onPick: () => void; + className: string; + title: string; + children: ReactNode; +} & Record) { + return ( + + ); +} + +const LogRow = memo(function LogRow({ event, - index, + view, + top, + height, + selected, onInspect, + onFilter, }: { event: LogEvent; - index: number; + view: Shaped; + top: number; + height: number; + selected: boolean; onInspect: (sequence: number) => void; + onFilter: (patch: Partial) => void; }) { - const display = cached(event); + const slow = view.durationMs !== null ? durationTone(view.durationMs) : null; return (
-
); }); +/* ---------- the drawer ---------- */ + +function Drawer({ + event, + view, + onClose, +}: { + event: LogEvent; + view: Shaped; + onClose: () => void; +}) { + const [copied, setCopied] = useState(false); + const leftovers = view.fields.filter(([key]) => !isSectioned(key) && key !== 'component'); + + const copy = async () => { + try { + await navigator.clipboard.writeText(JSON.stringify(event, null, 2)); + setCopied(true); + window.setTimeout(() => setCopied(false), 1600); + } catch { + setCopied(false); + } + }; + + const sections = DRAWER_SECTIONS.map((section) => ({ + title: section.title, + rows: section.keys + .map((key) => [key, view.attributes[key]] as [string, unknown]) + .filter(([, value]) => value !== undefined && value !== null && String(value) !== ''), + })).filter((section) => section.rows.length > 0); + + return ( +
+
+
+

+ + {view.service} + + + {view.component} +

+ {view.summary} + {view.detail ?

{view.detail}

: null} +
+
+ + +
+
+ +
+
+

Overview

+
+
Time
+
+ {view.day} {view.time} +
+
Level
+
{view.level}
+
Service
+
+ {view.service} › {view.component} +
+
Event
+
{view.eventKey}
+ {view.result ? ( + <> +
Result
+
{view.result.label}
+ + ) : null} + {view.durationMs !== null ? ( + <> +
Duration
+
{formatDuration(view.durationMs)}
+ + ) : null} +
Record
+
#{event.sequence}
+
+
+ + {sections.map((section) => ( +
+

{section.title}

+
+ {section.rows.map(([key, value]) => ( + +
{readableKey(key)}
+
{String(value)}
+
+ ))} +
+
+ ))} + + {leftovers.length ? ( +
+

Details

+
+ {leftovers.map(([key, value]) => ( + +
{readableKey(key)}
+
{String(value)}
+
+ ))} +
+
+ ) : null} +
+ +
+ Raw event +
{JSON.stringify(event, null, 2)}
+
+
+ ); +} + +/* ---------- the page ---------- */ + export function LogsPage() { const [records, setRecords] = useState([]); const [dropped, setDropped] = useState(0); const [paused, setPaused] = useState(false); - const [level, setLevel] = useState('INFO'); - const [search, setSearch] = useState(''); + const [held, setHeld] = useState(0); + const [filters, setFilters] = useState(EMPTY_FILTERS); const [error, setError] = useState(''); const [viewport, setViewport] = useState({ top: 0, height: 600 }); + const [atTail, setAtTail] = useState(true); const [selectedSequence, setSelectedSequence] = useState(null); - const deferredSearch = useDeferredValue(search.trim().toLowerCase()); + const deferredSearch = useDeferredValue(filters.text.trim().toLowerCase()); const cursor = useRef(0); const fetching = useRef(false); const viewGeneration = useRef(0); const view = useRef(null); const pinned = useRef(true); const scrollFrame = useRef(undefined); + // Arrivals while paused. Held here rather than left on the server: the ring buffer is + // finite, and a pause taken in order to read something is exactly when it must not be + // overwritten underneath the operator. + const holding = useRef([]); + // Read by `drain`, which is a timer callback rather than a render. Written in an effect + // rather than during render: a render that concurrent React discards must not be able to + // decide whether the next batch of arrivals is shown or held. + const pausedRef = useRef(paused); + useEffect(() => { + pausedRef.current = paused; + }, [paused]); + + const admit = useCallback((batch: LogEvent[]) => { + setRecords((current) => { + const next = current.concat(batch); + return next.length > RETAIN ? next.slice(next.length - RETAIN) : next; + }); + }, []); const drain = useCallback(async () => { - if (paused || fetching.current || document.hidden) return; + if (fetching.current || document.hidden) return; fetching.current = true; const generation = viewGeneration.current; const batch: LogEvent[] = []; @@ -148,23 +405,27 @@ export function LogsPage() { } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)); } finally { - // One React update for a complete catch-up prevents the initial 5,000-record drain - // from redrawing the page once per network page. + // One React update for a complete catch-up prevents the initial drain from redrawing + // the page once per network page. if (batch.length > 0 && generation === viewGeneration.current) { - setRecords((current) => { - const next = current.concat(batch); - return next.length > RETAIN ? next.slice(next.length - RETAIN) : next; - }); + if (pausedRef.current) { + holding.current = holding.current.concat(batch); + if (holding.current.length > RETAIN) { + holding.current = holding.current.slice(holding.current.length - RETAIN); + } + setHeld(holding.current.length); + } else { + admit(batch); + } } if (droppedInDrain > 0 && generation === viewGeneration.current) { setDropped((current) => current + droppedInDrain); } fetching.current = false; } - }, [paused]); + }, [admit]); useEffect(() => { - if (paused) return; let timer: number | undefined; const schedule = () => { window.clearInterval(timer); @@ -181,46 +442,121 @@ export function LogsPage() { window.clearInterval(timer); document.removeEventListener('visibilitychange', visibilityChanged); }; - }, [drain, paused]); + }, [drain]); - const filtered = useMemo(() => { - const minimum = RANKS[level] ?? 20; - return records.filter( - (event) => - (RANKS[event.level] ?? 0) >= minimum && - (!deferredSearch || cached(event).haystack.includes(deferredSearch)), - ); - }, [records, level, deferredSearch]); + // Both halves set the ref straight away as well as the state. A drain landing between + // the click and the commit would otherwise file its batch on the wrong side of the + // pause — held records with the button already reading "Pause", which nothing would + // ever flush. + const pause = useCallback(() => { + pausedRef.current = true; + setPaused(true); + }, []); + + const resume = useCallback(() => { + pausedRef.current = false; + const waiting = holding.current; + holding.current = []; + setHeld(0); + setPaused(false); + if (waiting.length) admit(waiting); + }, [admit]); + + const patch = useCallback((next: Partial) => { + setFilters((current) => ({ ...current, ...next })); + }, []); + + const filtered = useMemo( + () => records.filter((event) => matches(event, filters, deferredSearch)), + [records, filters, deferredSearch], + ); + + /* Row geometry. Two heights and a day divider mean the window cannot be found by + * dividing a scroll offset, so heights are accumulated once per filter change and the + * first visible row is found by binary search. Twenty thousand records is one pass over + * a typed array — cheaper than the render it replaces. */ + const layout = useMemo(() => { + const tops = new Float64Array(filtered.length + 1); + const heights = new Uint8Array(filtered.length); + const divider = new Uint8Array(filtered.length); + let y = 0; + let day = ''; + for (let index = 0; index < filtered.length; index += 1) { + const record = filtered[index]; + if (!record) continue; + const shaped = shape(record); + if (shaped.dayKey !== day) { + divider[index] = 1; + day = shaped.dayKey; + y += DAY_HEIGHT; + } + const height = shaped.tall ? ROW_TALL : ROW_COMPACT; + tops[index] = y; + heights[index] = height; + y += height; + } + tops[filtered.length] = y; + return { tops, heights, divider, total: y }; + }, [filtered]); + + const bodyTop = Math.max(0, viewport.top - HEADER_HEIGHT); + const first = useMemo(() => { + let low = 0; + let high = filtered.length; + while (low < high) { + const middle = (low + high) >> 1; + if ((layout.tops[middle] ?? 0) + (layout.heights[middle] ?? 0) <= bodyTop) low = middle + 1; + else high = middle; + } + return Math.max(0, low - OVERSCAN); + }, [layout, bodyTop, filtered.length]); + + const last = useMemo(() => { + const limit = bodyTop + viewport.height; + let index = first; + while (index < filtered.length && (layout.tops[index] ?? 0) < limit) index += 1; + return Math.min(filtered.length, index + OVERSCAN); + }, [layout, bodyTop, viewport.height, first, filtered.length]); + + const windowed = useMemo(() => { + const rows: { event: LogEvent; view: Shaped; index: number }[] = []; + for (let index = first; index < last; index += 1) { + const record = filtered[index]; + if (record) rows.push({ event: record, view: shape(record), index }); + } + return rows; + }, [filtered, first, last]); const lastSequence = filtered.at(-1)?.sequence ?? 0; - const bodyTop = Math.max(0, viewport.top - HEADER_HEIGHT); - const count = Math.ceil(viewport.height / ROW_HEIGHT) + OVERSCAN * 2; - // A restrictive filter can make the old scroll offset larger than the new body before - // the browser dispatches its compensating scroll event. Clamp immediately so that - // transition never paints an apparently empty log. - const first = Math.min( - Math.max(0, Math.floor(bodyTop / ROW_HEIGHT) - OVERSCAN), - Math.max(0, filtered.length - count), - ); - const windowed = filtered.slice(first, first + count); const selected = useMemo( () => records.find((event) => event.sequence === selectedSequence), [records, selectedSequence], ); + const scrollToTail = useCallback(() => { + const node = view.current; + if (!node) return; + pinned.current = true; + node.scrollTop = node.scrollHeight; + setAtTail(true); + setViewport({ top: node.scrollTop, height: node.clientHeight }); + }, []); + useLayoutEffect(() => { const node = view.current; if (!node || !pinned.current) return; node.scrollTop = node.scrollHeight; setViewport({ top: node.scrollTop, height: node.clientHeight }); - }, [lastSequence, deferredSearch, level]); + }, [lastSequence, layout.total]); useEffect(() => () => window.cancelAnimationFrame(scrollFrame.current ?? 0), []); const onScroll = () => { const node = view.current; if (!node) return; - pinned.current = node.scrollHeight - node.scrollTop - node.clientHeight < ROW_HEIGHT; + const tail = node.scrollHeight - node.scrollTop - node.clientHeight < ROW_TALL; + pinned.current = tail; + setAtTail(tail); window.cancelAnimationFrame(scrollFrame.current ?? 0); scrollFrame.current = window.requestAnimationFrame(() => { setViewport({ top: node.scrollTop, height: node.clientHeight }); @@ -228,7 +564,7 @@ export function LogsPage() { }; const exportJson = () => { - const blob = new Blob([JSON.stringify(records, null, 2)], { type: 'application/json' }); + const blob = new Blob([JSON.stringify(filtered, null, 2)], { type: 'application/json' }); const link = document.createElement('a'); link.href = URL.createObjectURL(blob); link.download = `memby-events-${new Date().toISOString().replace(/[:.]/g, '-')}.json`; @@ -236,39 +572,129 @@ export function LogsPage() { window.setTimeout(() => URL.revokeObjectURL(link.href), 1000); }; + const available = useMemo(() => facets(records), [records]); + const chips = activeChips(filters, available.services); + return ( <> -
- - patch({ service: event.target.value, component: '' })} + > + + {available.services.map((service) => ( + + ))} - - - setSearch(event.target.value)} - /> - -
-
+ +
+ - +
-
-