diff --git a/CLAUDE.md b/CLAUDE.md index 4e91940..69d6ca8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2130,7 +2130,7 @@ gained a duplicate on a copy-paste, looks identical to one that did not. The gre assembled separately so the authenticated name is trimmed once; the player also draws from the same tone-specific pool while it prepares a programme. -**Next up / auto-advance.** 30 s before an episode ends, `PlayerActivity` slides up +**Next up / auto-advance.** Inside the last minute of an episode, `PlayerActivity` fades in `player_next_up_banner.xml` and rolls into the next episode when it reaches zero (Settings → Playback turns it off; `Settings.autoPlayNextEpisode`). Which episode that is comes from `repository.nextEpisode`, dual-path like everything else: `/v1/items/{id}/next` on the @@ -2144,6 +2144,49 @@ inside the running player instead of relaunching the activity, so `itemId`/`play /`stopReported` must all be reset together or the outgoing episode is never reported stopped; and a movie simply resolves to null, which is why nothing special-cases item type. +**The bar is compact because the credits are the programme.** It was a 420dp card against +the end wall that shrank the picture to 58% and slid it left to make room for itself — so +the one part of an episode the overlay covered was its closing minute, which is the thing +the viewer is still watching. It is now a short bar in the bottom-left safe area: the +show's own title treatment, a hairline, `NEXT EPISODE` over the countdown, and a draining +ring. Nothing else — no episode thumbnail, no synopsis, no panel. Things to preserve: + +- **Nothing on it is focusable and it takes no focus**, which is what lets every gate it + used to appear in be *removed* rather than tightened. `seekControlsActive`, + `centrePausesPlayback` and `skipIntroCanShow` no longer mention it, so the centre key + still pauses and Left/Right still skip while it is up — the remote goes on meaning what + it meant a moment before the bar appeared. It is the only overlay in this player that + owns nothing, and `nextUpCanShow` is the other half of that bargain: wherever something + that *does* own the screen is up — the transport, which shares the same bottom edge, the + drop-up, the cast panel, the credits pane, the loading or error surfaces — the bar stands + down rather than being drawn underneath it, and comes back when they go. +- **The two actions survive without buttons.** Back dismisses, the same one-press-per-level + contract every other overlay here has; Play now is the transport's own Next Episode + control, which `shouldOfferNextEpisodeButton` offers under exactly the conditions this + bar appears under. A pair of pills on the bar would need focus, and focus is the thing + being given back. +- **The identity is bound when the episode is *resolved*, not when the bar is drawn.** + That is a minute or more of lead time, so the logo is already in the slot when the bar + fades in and the viewer never sees the name swapped for artwork — the swap + `setUpPlaybackIdentity` takes such care to avoid in the opposite corner. `nextUpBoundItemId` + is what keeps it to one bind per episode rather than one per 250 ms tick, and the text + fallback is shown meanwhile because a bar held back waiting on a logo is a bar that is + late for the thing it was announcing. +- **The ring is `CountdownRingView`**, shared with the skip-intro offer, which now differs + from it only in taking its colours from its own drawable state (that button inverts on + focus; this bar cannot be focused). Both are advanced from the *playhead* — a pause holds + the ring and a seek moves it — and both redraw only past a degree of movement, which over + a minute at 250 ms is most of the ticks skipped on a box that has a decoder to feed. +- **The figure and the line under the eyebrow are the same value**, from `formatRemaining`. + Two countdowns on one bar reading "60s" and "1:00" is a bar that looks broken. +- **`NEXT_UP_ACCENT` is written out rather than built with `Color.rgb`.** `PlayerActivity`'s + companion is initialised by plain JUnit tests with no Android framework under them, and + one android.graphics call from there fails every test in the file. +- Screenshots are `NextUpOverlayScreenshotTest` → `build/screenshots/next-up/`, over a + deliberately bright stand-in frame and captured at the full screen size rather than + cropped to the bar: whether the programme is still visible around it is the whole claim + the redesign makes, and it is not a thing a unit test can check. + **One next-item pipeline.** `ui/player/NextUpPipeline.kt` owns the answer to "what plays after this?", and everything that wants to know reads it from there: the manual Next Episode button, the next-up banner, the credits pane, the countdown and the ended frame. Before it, diff --git a/admin-ui/dist/assets/index-BIMcejkS.css b/admin-ui/dist/assets/index-BIMcejkS.css deleted file mode 100644 index b8c7406..0000000 --- a/admin-ui/dist/assets/index-BIMcejkS.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);--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 190px minmax(240px,1fr) 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:start;padding-top:7px;padding-bottom:7px;border-bottom:1px solid rgba(255,255,255,.03);contain:strict}.logrow>*,.logrow-line{line-height:16px}.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/16px 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/16px 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:center;gap:5px;min-width:0;height:16px;overflow:hidden}.logrow-service{flex:0 0 auto;max-width:96px;overflow:hidden;color:var(--quiet);font:700 10px/16px 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;line-height:16px}.logrow-summary{display:flex;flex-direction:column;gap:2px;align-items:stretch;justify-content:flex-start;width: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;height:16px}.logrow-action{flex:0 0 auto;color:var(--muted);font:600 10.5px/16px 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-trail{flex:0 1 auto;min-width:0;max-width:38%;overflow:hidden;color:var(--quiet);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.logrow-second{height:15px;overflow:hidden;color:var(--quiet);font-size:11px;line-height:15px;text-overflow:ellipsis;white-space:nowrap}.logrow-second[data-tone=error]{color:var(--danger-ink)}.logrow-result{min-width:0;height:16px;overflow:hidden}.logrow-verdict{display:inline-block;max-width:100%;color:var(--quiet);font:500 11px/14px 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/16px 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 170px minmax(200px,1fr) 130px;gap:10px}.loghead>:last-child,.logrow-duration{display:none}.loghead,.logbody{min-width:680px}}@media (max-width: 900px){.loghead,.logrow{grid-template-columns:46px 140px minmax(180px,1fr) 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}}.detail-row>td{padding:0 0 12px;background:var(--surface-sunken, transparent)}.logdrawer-raw h4{margin:0 0 6px;color:var(--quiet);font:700 10px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase}.notification-body{max-width:70ch;margin:0;color:var(--text);font:13px/1.6 var(--sans);overflow-wrap:anywhere}.subhead{display:flex;align-items:baseline;gap:8px;margin:6px 0 0;font-size:11.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}.subhead small{font-size:11px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--quiet)}.verdict{margin:0 0 16px;padding:12px 14px;border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--radius-sm);background:var(--surface-lift)}.verdict[data-tone=warn]{border-left-color:var(--warn)}.verdict[data-tone=bad]{border-left-color:var(--danger)}.verdict-head{display:flex;align-items:center;gap:9px;flex-wrap:wrap;font-size:13px;color:var(--muted)}.verdict-head svg{width:17px;height:17px;color:var(--accent)}.verdict[data-tone=warn] .verdict-head svg{color:var(--warn)}.verdict[data-tone=bad] .verdict-head svg{color:var(--danger)}.verdict-head b{color:var(--text);font-size:13px}.verdict-notes{margin:11px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}.verdict-notes li{display:flex;align-items:baseline;gap:9px;font-size:12.5px;color:var(--muted)}.metadata-hero-layout{display:grid;grid-template-columns:minmax(320px,.82fr) minmax(460px,1.35fr);gap:16px;align-items:start}.metadata-hero-order{display:grid;gap:8px}.metadata-hero-order-row{display:grid;grid-template-columns:30px minmax(0,1fr) auto;align-items:center;gap:10px;padding:10px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface-lift);opacity:.62}.metadata-hero-order-row[data-enabled]{opacity:1}.metadata-hero-order-number{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;background:var(--surface);color:var(--muted);font-size:12px;font-weight:700}.metadata-hero-order-actions{display:flex;gap:5px}.metadata-hero-order-actions button{min-width:34px;padding-inline:8px}.metadata-hero-colour{display:grid;gap:7px;margin-top:18px;padding-top:16px;border-top:1px solid var(--line-soft)}.metadata-hero-colour>b{font-size:13.5px}.metadata-hero-colour>p{margin:0;color:var(--muted);font-size:12.5px}.metadata-hero-colour .segments{width:fit-content}.metadata-hero-preview{position:relative;isolation:isolate;overflow:hidden;aspect-ratio:16 / 7.2;min-height:280px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#080b0e}.metadata-hero-preview:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;background:linear-gradient(90deg,#080b0e 0 34%,rgba(8,11,14,.9) 48%,rgba(8,11,14,.2) 76%,transparent)}.metadata-hero-preview-art{position:absolute;top:0;right:0;bottom:0;left:36%;z-index:-2;background:radial-gradient(circle at 67% 24%,rgba(211,242,255,.8) 0 2%,transparent 3%),radial-gradient(ellipse at 76% 78%,rgba(71,107,92,.8),transparent 45%),linear-gradient(145deg,#17303d,#456273 42%,#101a1f)}.metadata-hero-preview-copy{display:flex;flex-direction:column;justify-content:center;gap:10px;width:54%;min-height:100%;padding:26px 0 24px 28px}.metadata-preview-title{max-width:330px;color:#fff;font-family:Georgia,serif;font-size:clamp(24px,3vw,42px);line-height:.95;letter-spacing:.03em}.metadata-preview-ratings{display:flex;gap:8px}.metadata-preview-ratings b{padding:5px 8px;border-radius:4px;background:#ffffff1c;color:#e8edf0;font-size:11px}.metadata-preview-facts{display:flex;align-items:center;gap:7px;min-width:0;color:#bdc7ce;font-size:11px;line-height:16px;white-space:nowrap}.metadata-preview-facts span{overflow:hidden;text-overflow:ellipsis}.metadata-preview-facts b{flex:none;padding:1px 5px;border:1px solid rgba(255,255,255,.28);border-radius:3px;background:#00000038;color:#e8edf0;font-size:inherit;line-height:inherit}.metadata-preview-reason{overflow:hidden;color:var(--accent);font-size:11px;font-weight:600;line-height:16px;text-overflow:ellipsis;white-space:nowrap}.metadata-preview-progress{display:flex;align-items:center;gap:10px;color:#bdc7ce;font-size:11px;line-height:16px;white-space:nowrap}.metadata-preview-progress>span{display:block;overflow:hidden;width:112px;height:3px;border-radius:2px;background:#ffffff38}.metadata-preview-progress i{display:block;width:59%;height:100%;background:var(--accent)}.metadata-preview-progress b{color:var(--accent);font-size:inherit;line-height:inherit}.metadata-preview-progress[data-colour=white] b{color:#fff}.metadata-preview-summary{margin:0;color:#cbd2d7;font-size:12px;line-height:1.45}@media (max-width: 980px){.metadata-hero-layout{grid-template-columns:1fr}} diff --git a/admin-ui/dist/assets/index-BNwLRlCd.js b/admin-ui/dist/assets/index-BNwLRlCd.js new file mode 100644 index 0000000..f3b9078 --- /dev/null +++ b/admin-ui/dist/assets/index-BNwLRlCd.js @@ -0,0 +1,11 @@ +import{r as f,a as Gn,u as $s,L as J,b as Nn,m as Zn,N as Sn,O as Yn,c as He,B as Jn,R as Qn,d as V,e as Ps}from"./router-D9WH5XEU.js";(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const a of document.querySelectorAll('link[rel="modulepreload"]'))i(a);new MutationObserver(a=>{for(const l of a)if(l.type==="childList")for(const r of l.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&i(r)}).observe(document,{childList:!0,subtree:!0});function t(a){const l={};return a.integrity&&(l.integrity=a.integrity),a.referrerPolicy&&(l.referrerPolicy=a.referrerPolicy),a.crossOrigin==="use-credentials"?l.credentials="include":a.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function i(a){if(a.ep)return;a.ep=!0;const l=t(a);fetch(a.href,l)}})();var Cn={exports:{}},us={};/** + * @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 Xn=f,et=Symbol.for("react.element"),st=Symbol.for("react.fragment"),nt=Object.prototype.hasOwnProperty,tt=Xn.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,at={key:!0,ref:!0,__self:!0,__source:!0};function Mn(s,n,t){var i,a={},l=null,r=null;t!==void 0&&(l=""+t),n.key!==void 0&&(l=""+n.key),n.ref!==void 0&&(r=n.ref);for(i in n)nt.call(n,i)&&!at.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:et,type:s,key:l,ref:r,props:a,_owner:tt.current}}us.Fragment=st;us.jsx=Mn;us.jsxs=Mn;Cn.exports=us;var e=Cn.exports,Tn,_s=Gn;Tn=_s.createRoot,_s.hydrateRoot;const $n={overview:"M4 13h6V4H4zm0 7h6v-4H4zm10 0h6v-9h-6zm0-16v4h6V4z",library:"M3 5h18v14H3zM7 5v14M17 5v14M3 9.5h4M3 14.5h4M17 9.5h4M17 14.5h4",people:"M15 19v-1.2a3.3 3.3 0 0 0-3.3-3.3H6.8A3.3 3.3 0 0 0 3.5 17.8V19M9.2 11a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4ZM17 10.6a3 3 0 0 0-1.4-5.7M20.5 19v-1.2a3.3 3.3 0 0 0-2.4-3.2",person:"M18 20v-1.5a4 4 0 0 0-4-4h-4a4 4 0 0 0-4 4V20M12 10.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z",tv:"M4 5h16v10H4zM9 19h6M12 15v4M8 2.5 12 5l4-2.5",sliders:"M4 7h10M18 7h2M4 17h2m4 0h10M14 4v6M6 14v6",clock:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 7.2V12l3 1.8",pulse:"M3 12h3.5L9 19l5-14 2.5 7H21",chart:"M4 19V9m5 10V5m5 14v-7m5 7V3",chip:"M8 8h8v8H8zM4.5 4.5h15v15h-15zM9 2v2.5M15 2v2.5M9 19.5V22M15 19.5V22M2 9h2.5M2 15h2.5M19.5 9H22M19.5 15H22",database:"M12 8.2c4.4 0 8-1.2 8-2.6S16.4 3 12 3 4 4.2 4 5.6s3.6 2.6 8 2.6ZM4 5.6v12.8C4 19.8 7.6 21 12 21s8-1.2 8-2.6V5.6M4 12c0 1.4 3.6 2.6 8 2.6s8-1.2 8-2.6",download:"M12 3.5v10m0 0 4-4m-4 4-4-4M4.5 18h15",upload:"M12 16V4m0 0L8 8m4-4 4 4M5 13v6h14v-6",sync:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",search:"M10.5 17a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Zm4.6-1.9L20 20",star:"m12 3.2 2.6 5.4 5.9.8-4.3 4.1 1 5.9-5.2-2.8-5.2 2.8 1-5.9L3.5 9.4l5.9-.8L12 3.2Z",sparkle:"m10 3 1.5 4.3L16 8.8l-4.5 1.5L10 14.6 8.5 10.3 4 8.8l4.5-1.5L10 3ZM17.5 14l.9 2.4 2.6.9-2.6.9-.9 2.4-.9-2.4-2.6-.9 2.6-.9.9-2.4Z",bell:"M6.2 9.5a5.8 5.8 0 1 1 11.6 0c0 4.6 2.2 5.9 2.2 5.9H4s2.2-1.3 2.2-5.9M10 19.5a2 2 0 0 0 4 0",shield:"m12 3 7.5 3v5.4c0 5-3.2 8.2-7.5 9.6-4.3-1.4-7.5-4.6-7.5-9.6V6L12 3Zm-2.6 8.7 1.9 1.9 3.6-3.6",wrench:"m14.5 6.5 3-3 3 3-3 3M9 15l-5.5 5.5M13 4a5 5 0 0 0 6.5 6.5L10 20l-6-6 9.5-9.5Z",play:"M8 5.2v13.6L19 12 8 5.2ZM4 5v14",list:"M4 7h16M4 12h16M4 17h10",inbox:"M4 7h16v13H4zM8 4h8v3M8 12h8M8 16h5",send:"M21 3 10.5 13.5M21 3l-6.8 18-3.7-7.5L3 10z",history:"M3.5 12a8.5 8.5 0 1 0 2.8-6.3M3.5 4v4h4M12 7.5V12l3 1.8",check:"m5 12.5 4.5 4.5L19 7.5",alert:"M12 8.5v5m0 3.2h.01M10.3 4.4 2.7 17.5a2 2 0 0 0 1.7 3h15.2a2 2 0 0 0 1.7-3L13.7 4.4a2 2 0 0 0-3.4 0Z",power:"M12 3v9M7.5 6.2a7.5 7.5 0 1 0 9 0",key:"M14.5 3a6.5 6.5 0 1 0 3.4 12L19 14h2v-2h2V9.5l-2.5-2.5A6.5 6.5 0 0 0 14.5 3Zm-2.6 4.6a1.6 1.6 0 1 1-2.3 2.3 1.6 1.6 0 0 1 2.3-2.3Z",captions:"M4 5.5h16v13H4zM7 15h5m3 0h2M7 11h3m2 0h5",journey:"M4 6h5v5h6v7h5M7 3 4 6l3 3m10 6 3 3-3 3",plug:"M9 3v6M15 3v6M6.5 9h11v3.5a5.5 5.5 0 0 1-11 0zM12 18v3",calendar:"M4 6h16v15H4zM8 3v5M16 3v5M4 11h16",trash:"M4 7h16M9 7V4.5h6V7M6.5 7l1 13h9l1-13M10 11v5M14 11v5",plus:"M12 5v14M5 12h14",close:"M6 6l12 12M18 6 6 18",caret:"m6 9 6 6 6-6",external:"M14 4h6v6M20 4l-9 9M18 14v5.5H4.5V6H10",refresh:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",filter:"M3.5 5.5h17l-6.5 7.5V20l-4-2v-5L3.5 5.5Z",menu:"M4 7h16M4 12h16M4 17h16",globe:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3.5 9h17M3.5 15h17M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18Z",logout:"M15 17l5-5-5-5M20 12H9M12 4H5v16h7"};function oe({name:s,className:n}){const t=$n[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 ss({name:s,tone:n}){return $n[s]?e.jsx("span",{className:"glyph","data-tone":n,children:e.jsx(oe,{name:s})}):null}const K=(s,n,t,i,a,l,r={})=>({id:s,path:n,label:t,title:i,intro:a,icon:l,...r}),Rs=[{id:"dashboard",label:"Dashboard",defaultCollapsed:!1,collapsible:!1,items:[K("overview","/admin","Overview","Overview","What the gateway is doing right now.","overview")]},{id:"map",label:"Map",defaultCollapsed:!1,collapsible:!1,items:[K("journeys","/admin/journeys","Journeys","User journeys","How viewers move through Memby and complete flows.","journey"),K("journey-viewer","/admin/journeys/:userId","Journey","User journey","One viewer’s journey through Memby.","journey",{hidden:!0})]},{id:"history",label:"History",defaultCollapsed:!1,items:[K("activity","/admin/activity","Activity","Activity","Every administrative event, newest first.","bell",{badge:"notifications"}),K("logins","/admin/logins","Sign-ins","Sign-in history","Every connection attempt and whether it got in.","key"),K("notifications","/admin/notifications","Notifications","Notifications","Everything Memby sent and whether it worked.","send"),K("imports","/admin/imports","Imports","Imports","Catalogue synchronisation history.","database")]},{id:"stats",label:"Stats",defaultCollapsed:!1,items:[K("views","/admin/views","Views","App views","Home-screen visits, viewers and the times Memby is used.","overview"),K("engagement","/admin/engagement","Engagement","Row engagement","Impressions, focus, dwell and selections per launcher row.","chart"),K("searches","/admin/searches","Searches","Searches","What viewers have been looking for.","search")]},{id:"media",label:"Media",defaultCollapsed:!1,items:[K("library","/admin/library","Library","Library","Import and inspect the catalogue Memby ranks.","library"),K("requests","/admin/requests","Requests","Media requests","Who can ask for something the library does not have.","inbox"),K("recommendations","/admin/recommendations","Recommendations","For You","The prepared pools personalised rows are drawn from.","sparkle"),K("inspector","/admin/inspector","Score inspector","Score inspector","Re-run the ranker for one person and read every component.","search"),K("hero","/admin/hero","Home hero","Home hero","Choose films or television shows for the launcher spotlight.","star"),K("metadata-hero","/admin/metadata-hero","Metadata hero","Metadata hero","Order focused-title information shown above browse rows.","tv"),K("playback","/admin/playback","Playback","Playback","Presentation policy sent with every playback launch.","play"),K("subtitles","/admin/subtitles","Subtitles","Subtitles","Which providers a viewer may fetch a missing subtitle from.","captions"),K("credits","/admin/credits","Credits detection","Credits detection","Control predictive scanning and review completed scans.","clock")]},{id:"performance",label:"Performance",defaultCollapsed:!1,items:[K("runtime","/admin/runtime","Runtime","Runtime","Goroutines, memory and background workers.","chip"),K("logs","/admin/logs","Logs","Logs","Structured gateway events as they happen.","list"),K("updates","/admin/updates","App updates","App updates","Publish an optional or required client update.","upload"),K("tasks","/admin/tasks","Scheduled tasks","Scheduled tasks","What the gateway does in the background.","clock"),K("task","/admin/tasks/:taskId","Task","Task","One scheduled task and its run history.","clock",{hidden:!0})]},{id:"users",label:"Users",defaultCollapsed:!1,items:[K("accounts","/admin/accounts","Users","Users","Who uses Memby and the devices they are signed in on.","people"),K("account","/admin/accounts/:userId","User","User","Devices, recommendation setup and synced settings for one person.","person",{hidden:!0}),K("settings-history","/admin/accounts/:userId/settings","Settings history","Settings history","Changes to one person’s synced settings.","sliders",{hidden:!0}),K("clients","/admin/clients","Devices","Devices","Which sets have reported in and what they are running.","tv"),K("device","/admin/devices/:deviceId","Device","Device","One television’s connections and activity.","tv",{hidden:!0})]},{id:"rules",label:"Rules",defaultCollapsed:!1,items:[K("features","/admin/features","Client configuration","Client configuration","Control everything the thin TV client renders.","sliders"),K("notification-settings","/admin/notification-settings","Notification rules","TV notifications","Choose where notifications appear and who receives them.","bell"),K("webhooks","/admin/integrations/webhooks","Event webhooks","Event webhooks","Send administrative events to external services.","send")]},{id:"violations",label:"Violations",defaultCollapsed:!1,collapsible:!1,items:[K("media-reports","/admin/media-reports","Media reports","Media reports","Problems viewers reported with a film or episode.","alert")]},{id:"settings",label:"Settings",defaultCollapsed:!1,items:[K("integrations","/admin/integrations","Integrations","Integrations","External services Memby depends on.","plug"),K("integration","/admin/integrations/:integrationId","Integration","Integration","One external service, its settings and run history.","plug",{hidden:!0}),K("maintenance","/admin/maintenance","Maintenance","Maintenance","Take Memby offline or schedule quiet time.","wrench"),K("gateway-settings","/admin/settings","Gateway settings","Gateway settings","Timezone, logging and other server-level settings.","sliders",{hidden:!0})]}],it=Rs.flatMap(s=>s.items),rt=Rs.flatMap(s=>s.items.filter(n=>!n.path.includes(":")).map(n=>({...n,group:s.label}))),lt=2500,ot=2e3,ks=new Set,Ns=new Set;let Ee={reconnecting:!1,attempt:0},fs=!1,os,Le;function Ss(s){Ee=s,ks.forEach(n=>n(Ee))}function ct(){return new Promise(s=>{os=window.setTimeout(()=>{os=void 0,s()},lt)})}async function dt(){if(!fs){fs=!0;try{for(;Ee.reconnecting;){const s=Ee.attempt+1;Ss({reconnecting:!0,attempt:s}),Le=new AbortController;const n=window.setTimeout(()=>Le==null?void 0:Le.abort(),ot);try{if((await fetch("/healthz",{cache:"no-store",credentials:"same-origin",signal:Le.signal})).ok){Ss({reconnecting:!1,attempt:0}),Ns.forEach(i=>i());break}}catch{}finally{window.clearTimeout(n),Le=void 0}Ee.reconnecting&&await ct()}}finally{fs=!1,window.clearTimeout(os),os=void 0,Le=void 0}}}function Us(){Ee.reconnecting||Ss({reconnecting:!0,attempt:0}),dt()}function ht(s){return ks.add(s),s(Ee),()=>ks.delete(s)}function Es(s){return Ns.add(s),()=>Ns.delete(s)}function ut(){const[s,n]=f.useState(Ee);return f.useEffect(()=>ht(n),[]),s}class Ye extends Error{constructor(n,t,i=!1){super(n),this.status=t,this.temporaryAvailability=i,this.name="ApiError"}}function mt(s){return s===502||s===503||s===504}function pt(s){return s instanceof Ye&&s.temporaryAvailability}const xt=5*60*1e3;let Rn=Date.now();for(const s of["pointerdown","pointermove","keydown","wheel","scroll"])window.addEventListener(s,()=>{Rn=Date.now()},{passive:!0});const jt=()=>Date.now()-Rn({}));throw new Ye(i.error??`Request failed (${t.status})`,t.status)}if(t.status!==204)return await t.json()}function ze(s){const n=new URLSearchParams;for(const[i,a]of Object.entries(s))a==null||a===""||a===!1||n.set(i,String(a));const t=n.toString();return t?`?${t}`:""}const q={get:s=>ns(s),post:(s,n)=>ns(s,{method:"POST",body:n===void 0?void 0:JSON.stringify(n)}),put:(s,n)=>ns(s,{method:"PUT",body:n===void 0?void 0:JSON.stringify(n)}),del:s=>ns(s,{method:"DELETE"})},vt=3e4,En=f.createContext(null);function ft({children:s}){var h;const[n,t]=f.useState(),[i,a]=f.useState(!1),[l,r]=f.useState(""),[c,d]=f.useState(""),[o,x]=f.useState(!0),j=f.useRef(0),u=f.useCallback(async()=>{const p=++j.current;try{const w=await q.get("/admin/api/status");if(p!==j.current)return;t(w),a(!0),d("")}catch(w){if(p!==j.current)return;a(!1),d(w instanceof Error?w.message:String(w))}finally{p===j.current&&(r(new Date().toISOString()),x(!1))}},[]),v=f.useCallback(async p=>{var w;await q.post("/admin/api/maintenance",{enabled:p,message:((w=n==null?void 0:n.maintenance)==null?void 0:w.message)??""}),await u()},[u,(h=n==null?void 0:n.maintenance)==null?void 0:h.message]);f.useEffect(()=>{u();let p;const w=()=>{window.clearInterval(p),p=document.hidden?void 0:window.setInterval(()=>void u(),vt)},g=()=>{w(),document.hidden||u()};return w(),document.addEventListener("visibilitychange",g),()=>{window.clearInterval(p),document.removeEventListener("visibilitychange",g)}},[u]),f.useEffect(()=>Es(()=>void u()),[u]);const b=f.useMemo(()=>{var p;return{status:n,version:(n==null?void 0:n.serverVersion)??"",currentUser:((p=n==null?void 0:n.currentUser)==null?void 0:p.trim())||"Administrator",online:i,checkedAt:l,error:c,loading:o,reload:u,setMaintenance:v}},[n,i,l,c,o,u,v]);return e.jsx(En.Provider,{value:b,children:s})}function ve(){const s=f.useContext(En);if(!s)throw new Error("useGateway used outside GatewayProvider");return s}function bt(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 yt(){const s=$s(),{status:n}=ve(),[t,i]=f.useState(!1),[a,l]=f.useState(""),[r,c]=f.useState(0),d=f.useRef(null),o=f.useRef(null),x=f.useMemo(()=>{const u=a.trim().toLowerCase();return[...rt,...((n==null?void 0:n.requestUsers)??[]).map(b=>({id:`user-${b.id}`,path:`/admin/accounts/${encodeURIComponent(b.id)}`,label:b.username||"Unnamed user",title:`User: ${b.username||"Unnamed user"}`,intro:"Open this user’s devices and settings.",group:"Users",icon:"people"})),...((n==null?void 0:n.clients)??[]).map(b=>({id:`device-${b.deviceId}`,path:`/admin/devices/${encodeURIComponent(b.deviceId)}`,label:b.deviceName||"Unnamed device",title:`Device: ${b.deviceName||"Unnamed device"}`,intro:`${b.username||"Unknown user"} · ${b.version||"unknown version"}`,group:"Devices",icon:"tv"}))].map((b,h)=>({item:b,rank:u?bt(b,u):1,index:h})).filter(b=>b.rank>0).sort((b,h)=>h.rank-b.rank||b.index-h.index).map(({item:b,rank:h})=>({item:b,rank:h}))},[a,n]);f.useEffect(()=>c(0),[a]),f.useEffect(()=>{const u=v=>{var b;(b=d.current)!=null&&b.contains(v.target)||i(!1)};return document.addEventListener("pointerdown",u),()=>document.removeEventListener("pointerdown",u)},[]),f.useEffect(()=>{const u=v=>{var h,p;if(v.key!=="S"||!v.shiftKey||v.ctrlKey||v.metaKey||v.altKey)return;const b=document.activeElement;b&&(b.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(b.tagName))||(v.preventDefault(),(h=o.current)==null||h.focus(),(p=o.current)==null||p.select())};return window.addEventListener("keydown",u),()=>window.removeEventListener("keydown",u)},[]);const j=u=>{var v;i(!1),l(""),(v=o.current)==null||v.blur(),s(u)};return e.jsxs("div",{className:"omni",ref:d,"data-open":t||void 0,children:[e.jsxs("div",{className:"omni-input",children:[e.jsx(oe,{name:"search"}),e.jsx("input",{ref:o,type:"search",value:a,placeholder:"Search","aria-label":"Search","aria-expanded":t,onFocus:()=>i(!0),onChange:u=>{l(u.target.value),i(!0)},onKeyDown:u=>{var v;if(u.key==="Escape")l(""),i(!1),(v=o.current)==null||v.blur();else if(u.key==="ArrowDown")u.preventDefault(),c(b=>Math.min(b+1,x.length-1));else if(u.key==="ArrowUp")u.preventDefault(),c(b=>Math.max(b-1,0));else if(u.key==="Enter"){const b=x[r];if(!b)return;u.preventDefault(),j(b.item.path)}}}),e.jsx("span",{className:"omni-key",children:"⇧S"})]}),t?e.jsx("div",{className:"omni-panel",role:"listbox",children:x.length===0?e.jsx("p",{className:"empty",children:"No pages, users or devices match that search."}):x.map((u,v)=>e.jsxs("a",{className:v===r?"omni-item on":"omni-item",href:u.item.path,role:"option","aria-selected":v===r,onPointerEnter:()=>c(v),onClick:b=>{b.preventDefault(),j(u.item.path)},children:[u.item.icon?e.jsx(oe,{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 y=s=>(s??0).toLocaleString(),P=s=>s?new Date(s).toLocaleString():"—";function we(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 We(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 Ce(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 me(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(),Je=s=>`${Math.round((s??0)*100)}%`;function ge(s){if(!s)return"—";const n=Date.now()-new Date(s).getTime();if(n<0)return"just now";const t=Math.floor(n/1e3);if(t<45)return"just now";const i=Math.floor(t/60);if(i<60)return`${i} min ago`;const a=Math.floor(i/60);if(a<24)return`${a}h ago`;const l=Math.floor(a/24);return l<30?`${l}d ago`:new Date(s).toLocaleDateString()}function ms(s){if(!s)return"—";const n=new Date(s).getTime()-Date.now();if(!Number.isFinite(n))return"—";if(n<=0)return"due now";const t=Math.round(n/1e3);if(t<60)return`in ${t}s`;const i=Math.round(t/60);if(i<60)return`in ${i} min`;const a=Math.round(i/60);return a<24?`in ${a}h`:`in ${Math.round(a/24)}d`}const In=15*60*1e3,wt=3*60*60*1e3,ps=s=>!!s&&Date.now()-new Date(s).getTime(){var w;try{const g=await q.get(`/admin/api/notifications?limit=${Bs}`);t(g.events),a(g.unread),r(g.types),j.current=Math.max(j.current,((w=g.events[0])==null?void 0:w.id)??0),x("")}catch(g){x(g instanceof Error?g.message:String(g))}},[]),v=f.useCallback(w=>{j.current=Math.max(j.current,w.id),t(g=>g.some(m=>m.id===w.id)?g:[w,...g].sort((m,R)=>R.id-m.id).slice(0,Bs)),w.readAt||a(g=>g+1),r(g=>g.some(m=>m.type===w.type)?g.map(m=>m.type===w.type?{...m,count:m.count+1}:m):[...g,{type:w.type,count:1}])},[]);f.useEffect(()=>{u()},[u]),f.useEffect(()=>Es(()=>void u()),[u]),f.useEffect(()=>{let w=null,g,m=!1;return(()=>{m||(w=new EventSource(`/admin/api/notifications/stream?after=${j.current}`),w.addEventListener("open",()=>{d(!0),window.clearInterval(g),g=void 0}),w.addEventListener("admin",k=>{try{v(JSON.parse(k.data))}catch{}}),w.addEventListener("error",()=>{d(!1),g===void 0&&(g=window.setInterval(()=>void u(),kt))}))})(),()=>{m=!0,w==null||w.close(),window.clearInterval(g)}},[v,u]);const b=f.useCallback(async w=>{const g=w.filter(m=>m>0);if(g.length!==0){t(m=>m.map(R=>g.includes(R.id)&&!R.readAt?{...R,readAt:new Date().toISOString()}:R));try{const m=await q.post("/admin/api/notifications/read",{ids:g});a(m.unread)}catch{u()}}},[u]),h=f.useCallback(async()=>{a(0),t(w=>w.map(g=>g.readAt?g:{...g,readAt:new Date().toISOString()}));try{const w=await q.post("/admin/api/notifications/read",{all:!0});a(w.unread)}catch{u()}},[u]),p=f.useMemo(()=>({events:n,unread:i,types:l,connected:c,error:o,markRead:b,markAllRead:h,reload:u}),[n,i,l,c,o,b,h,u]);return e.jsx(Ln.Provider,{value:p,children:s})}function Is(){const s=f.useContext(Ln);if(!s)throw new Error("useNotifications used outside NotificationProvider");return s}function Dn(s){return s.severity==="error"?"bad":s.severity==="warning"?"warn":"info"}function qn(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 Hs=99;function St(){const{events:s,unread:n,connected:t,markRead:i,markAllRead:a}=Is(),[l,r]=f.useState(!1),c=f.useRef(null);return f.useEffect(()=>{const d=x=>{var j;(j=c.current)!=null&&j.contains(x.target)||r(!1)},o=x=>{x.key==="Escape"&&r(!1)};return document.addEventListener("pointerdown",d),document.addEventListener("keydown",o),()=>{document.removeEventListener("pointerdown",d),document.removeEventListener("keydown",o)}},[]),f.useEffect(()=>{if(!l)return;const d=s.filter(o=>!o.readAt).map(o=>o.id);d.length>0&&i(d)},[l]),e.jsxs("div",{className:"bell",ref:c,"data-open":l||void 0,children:[e.jsxs("button",{type:"button",className:"bell-button","aria-label":n>0?`Activity, ${n} unread`:"Activity","aria-expanded":l,onClick:()=>r(d=>!d),children:[e.jsx(oe,{name:"bell"}),n>0?e.jsx("span",{className:"bell-badge",children:n>Hs?`${Hs}+`:n}):null]}),l?e.jsxs("div",{className:"bell-panel",children:[e.jsxs("div",{className:"bell-head",children:[e.jsx("b",{children:"Activity"}),e.jsxs("div",{className:"row tight",children:[t?null:e.jsx("span",{className:"tag","data-tone":"warn",children:"reconnecting"}),n>0?e.jsx("button",{type:"button","data-variant":"quiet","data-size":"sm",onClick:()=>void a(),children:"Mark all read"}):null]})]}),e.jsx("div",{className:"bell-list",children:s.length===0?e.jsx("p",{className:"empty",children:"Nothing has happened yet."}):s.slice(0,20).map(d=>{const o=e.jsxs(e.Fragment,{children:[e.jsx(ss,{name:qn(d.type),tone:Dn(d)}),e.jsxs("span",{className:"bell-body",children:[e.jsx("b",{children:d.title||d.type}),d.summary?e.jsx("p",{children:d.summary}):null,e.jsx("time",{dateTime:d.occurredAt,children:ge(d.occurredAt)})]})]});return d.link?e.jsx(J,{className:"bell-item","data-unread":!d.readAt||void 0,to:d.link,onClick:()=>r(!1),children:o},d.id):e.jsx("div",{className:"bell-item","data-unread":!d.readAt||void 0,children:o},d.id)})}),e.jsx("div",{className:"bell-foot",children:e.jsx(J,{to:"/admin/activity",onClick:()=>r(!1),children:"All activity"})})]}):null]})}function Q(s,n={}){const{pollMs:t,enabled:i=!0}=n,[a,l]=f.useState(),[r,c]=f.useState(""),[d,o]=f.useState(i),[x,j]=f.useState(!1),u=f.useRef(0),v=f.useRef(!1),b=f.useCallback(async()=>{if(!i)return;const h=++u.current;v.current&&j(!0);try{const p=await q.get(s);if(h!==u.current)return;l(p),c(""),v.current=!0}catch(p){if(h!==u.current)return;c(p instanceof Error?p.message:String(p))}finally{h===u.current&&(o(!1),j(!1))}},[s,i]);return f.useEffect(()=>(v.current=!1,o(!0),b(),()=>{u.current+=1}),[b]),f.useEffect(()=>Es(()=>void b()),[b]),f.useEffect(()=>{if(!t||!i)return;let h;const p=()=>{window.clearInterval(h),h=document.hidden?void 0:window.setInterval(()=>void b(),t)},w=()=>{p(),document.hidden||b()};return p(),document.addEventListener("visibilitychange",w),()=>{window.clearInterval(h),document.removeEventListener("visibilitychange",w)}},[t,i,b]),{data:a,error:r,loading:d,refreshing:x,reload:b,set:l}}function ie(){const[s,n]=f.useState(null),t=f.useRef(!0);f.useEffect(()=>()=>{t.current=!1},[]);const i=f.useCallback(async(a,l)=>{n(a);try{return await l(),!0}finally{t.current&&n(null)}},[]);return{busy:s,run:i}}const zs="Memby";function Ct(s){f.useEffect(()=>{const n=s.trim();document.title=n?`${n} | ${zs}`:zs},[s])}function B({title:s,intro:n,actions:t,crumbs:i,icon:a}){var c;const l=Nn(),r=a??((c=it.find(d=>Zn({path:d.path,end:!0},l.pathname)))==null?void 0:c.icon);return Ct(s),e.jsxs("header",{className:"page-head",children:[i?e.jsx("nav",{className:"crumbs",children:i}):null,e.jsxs("div",{className:"page-head-row",children:[e.jsxs("div",{className:"page-head-title",children:[r?e.jsx("span",{className:"page-head-icon","aria-hidden":"true",children:e.jsx(oe,{name:r})}):null,e.jsxs("div",{className:"page-head-text",children:[e.jsx("h1",{children:s}),n?e.jsx("p",{children:n}):null]})]}),t?e.jsx("div",{className:"page-head-actions",children:t}):null]})]})}function T({title:s,intro:n,icon:t,tone:i,actions:a,footer:l,children:r}){return e.jsxs("section",{className:"card",children:[s?e.jsxs("div",{className:"card-head",children:[t?e.jsx(ss,{name:t,tone:i}):null,e.jsxs("div",{className:"card-head-text",children:[e.jsx("h2",{children:s}),n?e.jsx("p",{children:n}):null]}),a?e.jsx("div",{className:"card-head-actions",children:a}):null]}):null,r,l?e.jsx("div",{className:"card-foot",children:l}):null]})}function pe({cols:s,children:n}){return e.jsx("div",{className:"grid","data-cols":s,children:n})}function ce({tiles:s}){return e.jsx("div",{className:"tiles",children:s.map(n=>e.jsxs("div",{className:"tile",children:[n.icon?e.jsx(ss,{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 S({children:s,tone:n}){return e.jsx("span",{className:"tag","data-tone":n,children:s})}function de({children:s,tone:n}){return e.jsx("span",{className:"chip","data-tone":n,children:s})}function ae({children:s}){return e.jsx("p",{className:"empty",children:s})}function X({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:a,busy:l,icon:r,type:c="button",title:d}){return e.jsxs("button",{type:c,className:"","data-variant":t,"data-size":i,disabled:a||l,onClick:n,title:d,children:[l?e.jsx("span",{className:"spinner"}):r?e.jsx(oe,{name:r}):null,s]})}function D({label:s,hint:n,children:t,grow:i}){return e.jsxs("label",{className:i?"field grow":"field",children:[e.jsx("span",{children:s}),t,n?e.jsx("small",{children:n}):null]})}function Y({label:s,hint:n,checked:t,onChange:i,disabled:a}){return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:t,disabled:a,onChange:l=>i(l.target.checked)}),e.jsx("span",{className:"switch"}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:s}),n?e.jsx("p",{children:n}):null]})]})}function 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 G({children:s}){return e.jsx("div",{className:"table-wrap",children:s})}function xs({data:s,labelOf:n,valueOf:t,toneOf:i,title:a}){if(s.length===0)return e.jsx(ae,{children:"Nothing in this window."});const l=s.map(c=>t(c)),r=Math.max(1,...l);return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"bars",children:s.map((c,d)=>{const o=t(c);return e.jsx("div",{className:"bar","data-tone":i==null?void 0:i(c),"data-empty":o===0||void 0,style:{height:`${Math.max(2,o/r*100)}%`},title:a?a(c):`${n(c,d)}: ${o}`},d)})}),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 Fn({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 U({message:s,onDismiss:n}){return s?e.jsxs("div",{className:"banner",role:"alert",children:[e.jsx(oe,{name:"alert"}),e.jsx("span",{children:s}),n?e.jsx("button",{type:"button",onClick:n,"aria-label":"Dismiss",children:e.jsx(oe,{name:"close"})}):null]}):null}function H({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 Se({title:s,body:n,confirmLabel:t="Confirm",destructive:i,busy:a,onConfirm:l,onCancel:r}){const c=f.useId(),d=f.useRef(null);return f.useEffect(()=>{var x;(x=d.current)==null||x.focus();const o=j=>{j.key==="Escape"&&r()};return document.addEventListener("keydown",o),()=>document.removeEventListener("keydown",o)},[r]),e.jsx("div",{className:"scrim",onPointerDown:o=>o.target===o.currentTarget&&r(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true","aria-labelledby":c,ref:d,tabIndex:-1,children:[e.jsx("h2",{id:c,children:s}),e.jsx("p",{children:n}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{onClick:r,variant:"quiet",children:"Cancel"}),e.jsx($,{onClick:l,variant:i?"danger":"primary",busy:a,children:t})]})]})})}function Ve({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 bs({children:s,aside:n}){return e.jsxs("h4",{className:"subhead",children:[e.jsx("span",{children:s}),n?e.jsx("small",{children:n}):null]})}function Ls({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 Mt({open:s,onNavigate:n}){const{unread:t}=Is(),[i,a]=f.useState(()=>{try{return JSON.parse(localStorage.getItem("memby-admin-nav-v2")??"{}")}catch{return{}}}),l=c=>{try{localStorage.setItem("memby-admin-nav-v2",JSON.stringify(c))}catch{}},r=(c,d=!1)=>{a(o=>{const x={...o},j=!(o[c]??d);return x[c]=j,l(x),x})};return e.jsx("nav",{className:"rail",id:"rail","data-open":s||void 0,"aria-label":"Console sections",children:Rs.map(c=>{const d=c.items.filter(j=>!j.hidden);if(d.length===0)return null;const o=c.collapsible!==!1,x=!o||!(i[c.id]??c.defaultCollapsed??!1);return e.jsxs("div",{className:"rail-group",children:[c.label&&o?e.jsxs("button",{type:"button",className:"rail-head","aria-expanded":x,onClick:()=>r(c.id,c.defaultCollapsed),children:[c.label,e.jsx(oe,{name:"caret",className:"ico caret"})]}):c.label?e.jsx("div",{className:"rail-head rail-head-static",children:c.label}):null,x?d.map(j=>e.jsx(Sn,{to:j.path,end:j.path==="/admin",onClick:n,className:({isActive:u})=>u?"on":"","aria-current":void 0,children:({isActive:u})=>e.jsxs("span",{style:{display:"contents"},ref:v=>{const b=v==null?void 0:v.parentElement;b&&(u?b.setAttribute("aria-current","page"):b.removeAttribute("aria-current"))},children:[j.icon?e.jsx(oe,{name:j.icon}):null,j.label,j.badge==="notifications"&&t>0?e.jsx("span",{className:"rail-badge",children:t>99?"99+":t}):null]})},j.id)):null]},c.id)})})}function Tt(){var k,E,L;const{version:s,currentUser:n,online:t,loading:i,status:a,setMaintenance:l}=ve(),[r,c]=f.useState(!1),[d,o]=f.useState(!1),[x,j]=f.useState(!1),[u,v]=f.useState(!1),b=Nn(),h=!!((k=a==null?void 0:a.maintenance)!=null&&k.enabled),p=!!((E=a==null?void 0:a.quietTime)!=null&&E.active),w=f.useRef(null),g=((L=Array.from(n.trim())[0])==null?void 0:L.toLocaleUpperCase("en-NZ"))||"A",m=a&&t&&!h&&!p?"ok":a||!i?"bad":"checking",R=async()=>{if(!(x||!a)){j(!0);try{await l(!h)}finally{j(!1)}}};return f.useEffect(()=>c(!1),[b.pathname]),f.useEffect(()=>{if(!d)return;const A=_=>{var se;(se=w.current)!=null&&se.contains(_.target)||o(!1)},N=_=>{_.key==="Escape"&&o(!1)};return document.addEventListener("mousedown",A),window.addEventListener("keydown",N),()=>{document.removeEventListener("mousedown",A),window.removeEventListener("keydown",N)}},[d]),f.useEffect(()=>{if(!r)return;const A=document.body.style.overflow;document.body.style.overflow="hidden";const N=_=>{_.key==="Escape"&&c(!1)};return window.addEventListener("keydown",N),()=>{document.body.style.overflow=A,window.removeEventListener("keydown",N)}},[r]),e.jsxs(e.Fragment,{children:[e.jsx("a",{className:"skip",href:"#main",children:"Skip to content"}),e.jsxs("header",{className:"topbar",children:[e.jsxs("a",{className:"topbar-brand",href:"/admin",children:[e.jsx("span",{className:"brand-mark","aria-hidden":"true",children:"M"}),e.jsx("span",{className:"brand-word",children:"Memby Gateway"})]}),e.jsx("button",{type:"button",className:"rail-toggle","aria-label":"Sections","aria-expanded":r,"aria-controls":"rail",onClick:()=>c(A=>!A),children:e.jsx(oe,{name:"menu"})}),e.jsx("div",{className:"topbar-spacer"}),e.jsxs("div",{className:"topbar-tools",children:[e.jsx(yt,{}),e.jsxs("span",{className:"topbar-version",children:["gateway ",s||"unknown"]}),e.jsx("button",{type:"button",className:"topbar-status","data-tone":m,"aria-pressed":h,disabled:!a||!t||x||p,title:a?p?"Memby quiet time is active":h?"Bring Memby back online":t?"Take Memby offline":"Memby is not responding":"Checking Memby status","aria-label":p?"Memby quiet time is active":h?"Memby is offline. Bring it online":a&&t?"Memby is online. Take it offline":i?"Checking Memby status":"Memby is not responding",onClick:()=>h?void R():v(!0),children:e.jsx("span",{className:"dot","aria-hidden":"true"})}),e.jsx(St,{}),e.jsxs("div",{className:"account-menu","data-open":d||void 0,ref:w,children:[e.jsxs("button",{type:"button",className:"account-trigger","aria-haspopup":"menu","aria-expanded":d,"aria-label":`Signed in as ${n}`,onClick:()=>o(A=>!A),children:[e.jsx("span",{className:"account-avatar","aria-hidden":"true",children:g}),e.jsx("span",{className:"account-name",children:n}),e.jsx(oe,{name:"caret",className:"ico account-caret"})]}),d?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:g}),e.jsxs("span",{children:[e.jsx("small",{children:"Signed in as"}),e.jsx("b",{children:n})]})]}),e.jsxs(Sn,{to:"/admin/settings",role:"menuitem",onClick:()=>o(!1),children:[e.jsx(oe,{name:"sliders"}),"Gateway settings"]}),e.jsx("form",{method:"post",action:"/admin/logout",children:e.jsxs("button",{type:"submit",role:"menuitem",children:[e.jsx(oe,{name:"logout"}),"Log out"]})})]}):null]})]})]}),r?e.jsx("button",{type:"button",className:"rail-scrim","aria-label":"Close sections",onClick:()=>c(!1)}):null,e.jsx(Mt,{open:r,onNavigate:()=>c(!1)}),e.jsx("main",{className:"page",id:"main",children:e.jsx(Yn,{})}),u?e.jsx(Se,{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:x,onConfirm:()=>{v(!1),R()},onCancel:()=>v(!1)}):null]})}const On=f.createContext(null),$t=5e3;function Rt({children:s}){const[n,t]=f.useState([]),i=f.useRef(1),a=f.useCallback(d=>{t(o=>o.filter(x=>x.id!==d))},[]),l=f.useCallback((d,o="ok")=>{const x=i.current++;t(j=>[...j,{id:x,message:d,tone:o}]),window.setTimeout(()=>a(x),$t)},[a]),r=f.useCallback(async(d,o)=>{try{const x=await d();return o&&l(o,"ok"),x}catch(x){if(pt(x))return;l(x instanceof Error?x.message:String(x),"bad");return}},[l]),c=f.useMemo(()=>({show:l,wrap:r}),[l,r]);return e.jsxs(On.Provider,{value:c,children:[s,e.jsx("div",{className:"toasts",role:"status","aria-live":"polite",children:n.map(d=>e.jsxs("div",{className:"toast","data-tone":d.tone,children:[e.jsx(oe,{name:d.tone==="bad"?"alert":"check"}),e.jsx("span",{children:d.message}),e.jsx("button",{type:"button",onClick:()=>a(d.id),"aria-label":"Dismiss",children:e.jsx(oe,{name:"close"})})]},d.id))})]})}function le(){const s=f.useContext(On);if(!s)throw new Error("useToast used outside ToastProvider");return s}function Et(){const{reconnecting:s,attempt:n}=ut();return s?e.jsx("div",{className:"reconnect-overlay",role:"status","aria-live":"polite","aria-label":"Server updating",children:e.jsxs("div",{className:"reconnect-panel",children:[e.jsx("span",{className:"reconnect-spinner","aria-hidden":"true"}),e.jsx("h1",{children:"Server Updating..."}),e.jsx("p",{children:"The backend is restarting or temporarily unavailable. Your Admin UI will reconnect automatically."}),e.jsxs("small",{children:["Attempting connection... Attempt ",n]})]})}):null}const Ks={ok:"ok",watch:"warn",bad:"bad"},At={ok:"Healthy",watch:"Worth watching",bad:"Needs attention"};function Pn({health:s,compact:n}){const t=s.notes??[];return e.jsxs("div",{className:"verdict","data-tone":Ks[s.level]??"ok",children:[e.jsxs("div",{className:"verdict-head",children:[e.jsx(oe,{name:s.level==="ok"?"check":"alert"}),e.jsx("b",{children:At[s.level]??"Unknown"}),e.jsx("span",{children:s.summary})]}),n||t.length===0?null:e.jsx("ul",{className:"verdict-notes",children:t.map((i,a)=>e.jsxs("li",{children:[e.jsx(S,{tone:Ks[i.level]??"note",children:i.area}),e.jsx("span",{children:i.message})]},`${i.area}-${a}`))})]})}function cs(s,n){switch(s.direction){case"rising":return`rising ${n(Math.abs(s.perHour))} an hour`;case"falling":return`falling ${n(Math.abs(s.perHour))} an hour`;case"steady":return"steady";default:return"gathering history"}}function _n(s){if(!s||s<60)return`${Math.max(0,Math.round(s))}s`;const n=Math.floor(s/60);if(n<60)return`${n} min`;const t=Math.floor(n/60);return t<48?`${t}h ${n%60}m`:`${Math.floor(t/24)} days`}function It(){var w,g,m,R,k,E;const{status:s,error:n,loading:t}=ve(),i=Q("/admin/api/runtime",{pollMs:3e4}),a=Q("/admin/api/views",{pollMs:6e4});if(t||!s)return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Overview",intro:"What the gateway is doing right now."}),e.jsx(U,{message:n}),e.jsx(H,{})]});const l=s.features??{features:[],revision:0,safeMode:!1},r=l.features??[],c=s.clients??[],d=c.filter(L=>ps(L.lastSeen)).length,o=s.updatePolicy??{},x=!!o.minimumVersion&&o.minimumVersion===o.latestVersion,j=s.playbackPolicy,u=s.mdblist,v=s.forYou,b=(s.runs??[]).slice(0,5),h=i.data,p=h?h.memory.memoryLimit>0&&h.memory.memoryLimitL.enabled).length} / ${r.length}`,icon:"sliders",tone:"ok"},{label:"last import",value:P(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsxs(pe,{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(Ve,{rows:[{label:"Availability",value:(k=s.maintenance)!=null&&k.enabled?e.jsx(S,{tone:"bad",children:"offline for maintenance"}):(E=s.quietTime)!=null&&E.active?e.jsx(S,{tone:"warn",children:"quiet time active"}):e.jsx(S,{tone:"ok",children:"online"})},{label:"Feature control plane",value:l.safeMode?e.jsx(S,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(S,{tone:"ok",children:["revision r",y(l.revision)]})},{label:"App update prompt",value:o.enabled?e.jsxs(S,{tone:x?"warn":"ok",children:[x?"required · ":"optional · ",o.latestVersion]}):e.jsx(S,{children:"off"})},{label:"Catalogue import",value:s.syncRunning?e.jsx(S,{tone:"warn",children:"running"}):e.jsxs(S,{children:["every ",s.syncEvery]})},{label:"Playback preroll",value:(j==null?void 0:j.prerollEnabled)===!1?e.jsx(S,{children:"off"}):e.jsxs(S,{tone:"ok",children:[((j==null?void 0:j.prerollDurationMs)??6500)/1e3,"s"]})}]})}),e.jsx(T,{title:"Services",intro:"The services this gateway leans on, and whether they answered.",icon:"wrench",tone:"note",actions:e.jsx(J,{to:"/admin/integrations",children:"Integrations"}),children:e.jsx(Ve,{rows:[{label:"Movies (Radarr)",value:s.radarrReady?e.jsx(S,{tone:"ok",children:"ready"}):e.jsx(S,{children:"not configured"})},{label:"Series (Sonarr)",value:s.sonarrReady?e.jsx(S,{tone:"ok",children:"ready"}):e.jsx(S,{children:"not configured"})},{label:"MDBList ratings",value:u!=null&&u.enabled?e.jsxs(S,{tone:"ok",children:[y(u.cachedTitles)," titles stored"]}):e.jsx(S,{children:u!=null&&u.apiKeyConfigured?"off · key saved":"off · no key"})},{label:"For You pools",value:s.forYouRunning?e.jsx(S,{tone:"warn",children:"rebuilding"}):e.jsxs(S,{children:[y((v==null?void 0:v.candidates)??0)," ranked candidates"]})},{label:"Recommendation profiles",value:e.jsx("span",{className:"mono",children:y((v==null?void 0:v.profiles)??0)})}]})})]}),e.jsxs(pe,{cols:"2",children:[e.jsx(T,{title:"Latest imports",intro:"The last few catalogue synchronisations.",icon:"sync",tone:"data",actions:e.jsx(J,{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:b.length===0?e.jsx(X,{columns:4,children:"No imports have run yet."}):b.map(L=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(L.startedAt)}),e.jsx("td",{children:L.kind}),e.jsx("td",{children:e.jsx(S,{tone:L.status==="success"?"ok":L.status==="running"?"warn":"bad",children:L.status})}),e.jsx("td",{className:"num",children:y(L.itemsUpserted)})]},L.id||L.startedAt))})]})})}),e.jsx(T,{title:"Process",intro:"The container the gateway is served from.",icon:"chip",tone:"info",actions:e.jsx(J,{to:"/admin/runtime",children:e.jsx($,{size:"sm",variant:"quiet",icon:"external",children:"Runtime details"})}),children:h?e.jsxs(e.Fragment,{children:[e.jsx(Pn,{health:h.health,compact:!0}),e.jsx(Ls,{tiles:[{label:`heap used · ${cs(h.heapTrend,me)}`,value:me(h.memory.heapInuse)},{label:"of the process limit",value:p?`${(h.memory.heapShare*100).toFixed(1)}%`:"no limit"},{label:`goroutines · ${cs(h.goroutineTrend,L=>y(Math.round(L)))}`,value:y(h.goroutines)},{label:"running for",value:_n(h.uptimeSeconds)}]}),e.jsxs("p",{className:"hint",children:["Heap in use is what the gateway is holding;"," ",me(h.memory.sys)," is reserved from the operating system on its behalf, which is always larger and is not a leak.",p?` The limit is ${me(h.memory.memoryLimit)}${h.memory.configuredLimit?" (GOMEMLIMIT)":""}.`:" No memory limit is set."]})]}):e.jsx("p",{className:"hint",children:"Reading process statistics…"})})]})]})}const Lt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"}];function Dt(){const{unread:s,connected:n,markAllRead:t,reload:i}=Is(),[a,l]=f.useState(7),[r,c]=f.useState(""),[d,o]=f.useState(""),[x,j]=f.useState(!1),[u,v]=f.useState(0),b=100,h=f.useMemo(()=>`/admin/api/notifications${ze({days:a,type:r,severity:d,unread:x,limit:b,offset:u*b})}`,[a,r,d,x,u]),{data:p,error:w,loading:g,reload:m}=Q(h),R=async()=>{await t(),await m()};return e.jsxs(e.Fragment,{children:[e.jsx(B,{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 R(),icon:"check",children:"Mark all read"}):void 0}),e.jsx(U,{message:w}),e.jsx(ce,{tiles:[{label:"Events in window",value:y((p==null?void 0:p.total)??0),icon:"bell",tone:"info"},{label:"Unread",value:y(s),icon:"alert",tone:s>0?"warn":void 0},{label:"Kinds seen",value:y((p==null?void 0:p.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(Be,{value:a,options:Lt.map(k=>({value:k.value,label:k.label})),onChange:k=>{l(k),v(0)}})}),e.jsx(D,{label:"Kind",children:e.jsxs("select",{value:r,onChange:k=>{c(k.target.value),v(0)},children:[e.jsx("option",{value:"",children:"Everything"}),((p==null?void 0:p.types)??[]).map(k=>e.jsxs("option",{value:k.type,children:[Vs(k.type)," (",k.count,")"]},k.type))]})}),e.jsx(D,{label:"Severity",children:e.jsxs("select",{value:d,onChange:k=>{o(k.target.value),v(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:x?"unread":"",onChange:k=>{j(k.target.value==="unread"),v(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:()=>{m(),i()},children:"Refresh"})})]}),g?e.jsx(H,{}):e.jsx(T,{title:"Events",icon:"bell",tone:"info",footer:((p==null?void 0:p.total)??0)>b?e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",disabled:u===0,onClick:()=>v(u-1),children:"Newer"}),e.jsx($,{size:"sm",disabled:(u+1)*b>=((p==null?void 0:p.total)??0),onClick:()=>v(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:((p==null?void 0:p.events.length)??0)===0?e.jsx(X,{columns:6,children:"Nothing has happened in this window."}):p==null?void 0:p.events.map(k=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(k.occurredAt),children:ge(k.occurredAt)}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(ss,{name:qn(k.type),tone:Dn(k)}),Vs(k.type)]})}),e.jsxs("td",{children:[e.jsx("b",{children:k.title}),k.summary?e.jsx("div",{className:"muted",children:k.summary}):null]}),e.jsx("td",{className:"muted nowrap",children:k.actor||"—"}),e.jsx("td",{className:"muted nowrap",children:k.target||"—"}),e.jsxs("td",{className:"nowrap",children:[k.readAt?null:e.jsx(S,{tone:"ok",children:"new"}),k.link?e.jsx(J,{className:"table-row-link",to:k.link,children:"Open"}):null]})]},k.id))})]})})})]})}function Gs(s){const n=s?new Date(s).getTime():0;return Number.isFinite(n)?n:0}function Zs(){return e.jsx("span",{className:"muted",title:"No Tracearr sessions matched to this person",children:"—"})}function qt(){var A;const{data:s,error:n,loading:t,reload:i}=Q("/admin/api/accounts",{pollMs:6e4}),{data:a}=Q("/admin/api/status",{pollMs:6e4}),{busy:l,run:r}=ie(),{wrap:c}=le(),[d,o]=f.useState(null),[x,j]=f.useState(null),u=(s==null?void 0:s.accounts)??[],v=u.flatMap(N=>N.devices??[]),b=((A=a==null?void 0:a.updatePolicy)==null?void 0:A.latestVersion)??"",h=N=>!!(N&&b&&N===b),p=v.filter(N=>N.version&&b&&N.version!==b).length,w=u.filter(N=>{var _;return(_=N.watchTime)==null?void 0:_.matched}),g=w.reduce((N,_)=>{var se;return N+(((se=_.watchTime)==null?void 0:se.weekMs)??0)},0),m=[...u].sort((N,_)=>Gs(_.lastSeen)-Gs(N.lastSeen)),R=(N,_)=>`/admin/api/accounts/${encodeURIComponent(N)}${_}`,k=async(N,_)=>r(`notifications-${N.id}`,()=>c(()=>q.put(R(N.id,"/notifications"),{...N.notifications,enabled:_}),`Notifications ${_?"enabled":"disabled"} for ${N.username}`).then(()=>i())),E=async N=>{const _=!N.enabled;await r(`enabled-${N.id}`,()=>c(()=>q.put(R(N.id,"/enabled"),{enabled:_}),`${N.username} ${_?"enabled":"disabled"}`).then(()=>(j(null),i())))},L=async N=>r(`update-${N.id}`,()=>c(()=>q.post(R(N.id,"/force-update")),`Update request queued for ${N.username}`));return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Users",intro:"Manage Memby users and signed-in devices at a glance."}),e.jsx(U,{message:n}),t?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"Memby users",value:y(u.length),icon:"people",tone:"note"},{label:"signed-in devices",value:y(v.length),icon:"tv",tone:"info"},{label:"active in the last 15 mins",value:y(v.filter(N=>ps(N.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"devices up to date",value:y(v.filter(N=>h(N.version)).length),icon:"check",tone:"ok"},{label:"devices requiring update",value:y(p),icon:"alert",tone:p?"warn":"note"},...w.length?[{label:"watch time this week",value:We(g),icon:"pulse",tone:"data"}]:[]]}),e.jsx(T,{title:"Users",icon:"people",tone:"note",children:e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"User"}),e.jsx("th",{className:"num",children:"Devices"}),e.jsx("th",{children:"Version"}),e.jsx("th",{children:"Remote IP"}),e.jsx("th",{className:"num",children:"This week"}),e.jsx("th",{className:"num",children:"This month"}),e.jsx("th",{children:"Last seen"}),e.jsx("th",{children:"Notifications"}),e.jsx("th",{children:"Enabled"}),e.jsx("th",{children:"Actions"})]})}),e.jsx("tbody",{children:m.length===0?e.jsx(X,{columns:10,children:"No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here."}):m.map(N=>{var re,C;const _=N.devices??[],se=As(N.lastSeen),ne=N.watchTime,te=d===N.id,O=_[0];return e.jsxs(e.Fragment,{children:[e.jsxs("tr",{className:N.enabled?void 0:"is-disabled",children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":se.tone,title:se.label}),e.jsx("span",{className:"avatar",children:N.initials||An(N.username)}),e.jsx(J,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(N.id)}`,children:N.username||"Unnamed user"})]})}),e.jsx("td",{className:"num",children:e.jsx("button",{type:"button",className:"link-button",onClick:()=>o(te?null:N.id),children:y(_.length)})}),e.jsxs("td",{children:[e.jsx("span",{title:O!=null&&O.version?`Latest recorded version on ${O.name}`:"Version not reported",children:(O==null?void 0:O.version)||e.jsx("span",{className:"muted",children:"Unknown"})}),O!=null&&O.version&&h(O.version)?e.jsx(S,{tone:"ok",children:"current"}):O!=null&&O.version&&b?e.jsx(S,{tone:"warn",children:"update"}):null,_.length>1?e.jsxs("span",{className:"table-sub",children:[_.length-1," more device",_.length===2?"":"s"]}):null]}),e.jsx("td",{className:"mono",title:"Last recorded IP address",children:(O==null?void 0:O.lastIp)||N.lastIp||e.jsx("span",{className:"muted",children:"—"})}),e.jsx("td",{className:"num",children:ne!=null&&ne.matched?We(ne.weekMs):e.jsx(Zs,{})}),e.jsx("td",{className:"num muted",children:ne!=null&&ne.matched?We(ne.monthMs):e.jsx(Zs,{})}),e.jsx("td",{className:"nowrap muted",title:P(N.lastSeen),children:ge(N.lastSeen)}),e.jsx("td",{children:e.jsx(Y,{label:(re=N.notifications)!=null&&re.enabled?"ON":"OFF",checked:!!((C=N.notifications)!=null&&C.enabled),disabled:l===`notifications-${N.id}`,onChange:F=>void k(N,F)})}),e.jsx("td",{children:e.jsx(Y,{label:N.enabled?"Enabled":"Disabled",checked:N.enabled,disabled:l===`enabled-${N.id}`,onChange:()=>j(N)})}),e.jsx("td",{children:e.jsx($,{size:"sm",variant:"quiet",busy:l===`update-${N.id}`,onClick:()=>void L(N),children:"Force update"})})]},N.id),te?e.jsx("tr",{className:"account-device-detail",children:e.jsx("td",{colSpan:10,children:e.jsx("div",{className:"device-breakdown",children:_.map(F=>e.jsxs("div",{className:"device-breakdown-row",children:[e.jsx("b",{children:F.name||"Unnamed device"}),e.jsx("span",{children:F.version||"Version unknown"}),e.jsx("span",{className:"mono",title:"Last recorded IP address",children:F.lastIp||"IP not recorded"}),e.jsxs("span",{className:"muted",children:["last seen ",ge(F.lastSeen)]})]},F.id))})})},`${N.id}-devices`):null]})})})]})})})]}),x?e.jsx(Se,{title:`${x.enabled?"Disable":"Enable"} ${x.username}?`,body:x.enabled?"This will stop their Memby sessions from accessing the gateway. Their Emby account is not changed.":"This will restore their access to Memby.",confirmLabel:x.enabled?"Disable user":"Enable user",destructive:x.enabled,busy:l===`enabled-${x.id}`,onCancel:()=>j(null),onConfirm:()=>void E(x)}):null]})}function ys(s){const n=String(s??"").replace("#","");return n.length!==8?`#${n}`:`#${n.slice(2)}${n.slice(0,2)}`}function Ft(){var Z,he,Ne,Ae,Ie,fe;const{userId:s=""}=He(),n=$s(),{wrap:t}=le(),{busy:i,run:a}=ie(),l=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:r,error:c,loading:d,reload:o}=Q("/admin/api/accounts",{pollMs:3e4}),[x,j]=f.useState(null),[u,v]=f.useState(null),[b,h]=f.useState(null),[p,w]=f.useState(null),[g,m]=f.useState(null),[R,k]=f.useState(null),E=Q(`${l}/viewers`),L=((r==null?void 0:r.accounts)??[]).find(I=>I.id===s),A=(((Z=E.data)==null?void 0:Z.viewers)??[]).filter(I=>I.kind!=="main"),N=!!((he=E.data)!=null&&he.enabled)&&A.length<(((Ne=E.data)==null?void 0:Ne.maxShadowViewers)??0),_=(r==null?void 0:r.catalogue)??[],se=(r==null?void 0:r.themes)??[];f.useEffect(()=>{var I;x===null&&L&&j({...((I=L.settings)==null?void 0:I.preferences)??{}})},[L,x]),f.useEffect(()=>{b===null&&L&&h({...L.notifications})},[L,b]),f.useEffect(()=>{if(u!==null||!L)return;const I=L.themes??[];v(I.length===0?se.map(W=>W.id):I)},[L,u,se]);const ne=f.useMemo(()=>{const I=[];for(const W of _){let ue=I.find(xe=>xe.name===W.area);ue||I.push(ue={name:W.area,definitions:[]}),ue.definitions.push(W)}return I},[_]),te=(I,W,ue,xe)=>a(I,async()=>{const qe=await t(W,ue);w(null),qe!==void 0&&(xe==null||xe()),await o()});if(d)return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"User",crumbs:e.jsx(J,{to:"/admin/accounts",children:"← All users"})}),e.jsx(H,{})]});if(!L)return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"User",crumbs:e.jsx(J,{to:"/admin/accounts",children:"← All users"})}),e.jsx(U,{message:c}),e.jsx(T,{children:e.jsx(ae,{children:"This user is no longer signed in to Memby."})})]});const O=L.devices??[],re=O.filter(I=>ps(I.lastSeen)).length,C=L.settings??{},F=L.recommendations??{};return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:L.username||"Unnamed user",intro:`Memby user · ${L.shortName?`greeted as ${L.shortName} · `:""}${y(O.length)} device${O.length===1?"":"s"} · last seen ${P(L.lastSeen)}`,crumbs:e.jsx(J,{to:"/admin/accounts",children:"← All users"}),actions:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"avatar",children:L.initials||An(L.username)}),re?e.jsxs(S,{tone:"ok",children:[re," active now"]}):e.jsx(S,{children:"idle"}),e.jsx(de,{children:L.id})]})}),e.jsx(U,{message:c}),e.jsxs(pe,{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:O.length===0?e.jsx(ae,{children:"No devices are signed in to this user."}):e.jsx("div",{className:"list",children:O.map(I=>{const W=As(I.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":W.tone,title:W.label})," ",e.jsx(J,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(I.id)}`,children:I.name||"Memby TV"})]}),e.jsxs("p",{children:[I.version?`Memby ${I.version}`:"Legacy Memby client"," · ",W.label," · last seen ",P(I.lastSeen)," · signed in ",P(I.signedInAt)]}),(I.versions??[]).length>0?e.jsx("div",{className:"chips",children:(I.versions??[]).map(ue=>e.jsxs(de,{tone:ue.version===I.version?"ok":void 0,children:[ue.version,ue.version===I.version?" · now":""]},ue.version))}):null]}),e.jsxs("div",{className:"list-actions",children:[e.jsx($,{size:"sm",disabled:!I.id,onClick:()=>m({id:I.id,name:I.name}),children:"Rename"}),e.jsx($,{size:"sm",variant:"danger",disabled:!I.id,onClick:()=>w({kind:"remove-device",deviceId:I.id,name:I.name}),children:"Sign out"})]})]},I.id||I.name)})})}),e.jsxs(T,{title:"Viewers",intro:"Several people under one Emby account, each with their own Continue Watching, watched history and favourites. Only the account itself is synced with Emby; a shadow viewer's watching is kept by Memby and never reported.",icon:"people",tone:"note",actions:E.data&&!E.data.enabled?e.jsx(S,{tone:"warn",children:"switched off"}):e.jsxs(S,{tone:"ok",children:[y(A.length)," beside the account"]}),footer:N?e.jsx($,{variant:"primary",onClick:()=>k({id:null,name:""}),children:"Add viewer"}):e.jsx($,{disabled:!0,children:E.data?`${y(E.data.maxShadowViewers)} is the limit`:"Add viewer"}),children:[E.data&&!E.data.enabled?e.jsx(U,{message:"Viewers are switched off for this server, so every television watches as the account. Nothing here is deleted — turn the feature on under Features to bring these people back."}):null,E.loading?e.jsx(H,{}):(((Ae=E.data)==null?void 0:Ae.viewers)??[]).length===0?e.jsx(ae,{children:"Nobody is set up yet, so this account has one viewer: itself."}):e.jsx("div",{className:"list",children:(((Ie=E.data)==null?void 0:Ie.viewers)??[]).map(I=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:I.name}),e.jsxs("p",{children:[I.kind==="main"?"The account itself — named by Emby, and the only one whose watching Emby hears about":"Watches privately; nothing reaches Emby",I.createdAt&&I.kind!=="main"?` · added ${P(I.createdAt)}`:""]})]}),e.jsx("div",{className:"list-actions",children:I.kind==="main"?e.jsx(S,{tone:"info",children:"synced with Emby"}):e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",onClick:()=>k({id:I.id,name:I.name}),children:"Rename"}),e.jsx($,{size:"sm",variant:"danger",onClick:()=>w({kind:"remove-viewer",viewer:I}),children:"Remove"})]})})]},I.id))})]}),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:F.completed?e.jsx($,{busy:i==="reset-rec",onClick:()=>w({kind:"reset-recommendations"}),children:"Clear stored choices"}):F.prompted?e.jsx($,{onClick:()=>w({kind:"cancel-prompt"}),children:"Cancel prompt"}):e.jsx($,{variant:"primary",busy:i==="prompt",onClick:()=>void te("prompt",()=>q.put(`${l}/recommendations/prompt`),"Setup prompt queued."),children:"Send setup prompt"}),children:[e.jsx("div",{className:"row tight",children:F.completed?e.jsx(S,{tone:"ok",children:"completed"}):F.prompted?e.jsx(S,{tone:"warn",children:"prompt queued"}):e.jsx(S,{children:"not invited"})}),e.jsx(Ot,{prompt:F})]})]}),(fe=L.watchTime)!=null&&fe.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 server's own timezone.",icon:"pulse",tone:"data",actions:L.watchTime.tracearrUsername?e.jsx(de,{children:L.watchTime.tracearrUsername}):null,children:e.jsx(ce,{tiles:[{label:`this week · ${y(L.watchTime.weekSessions)} session${L.watchTime.weekSessions===1?"":"s"}`,value:We(L.watchTime.weekMs),icon:"pulse",tone:"data"},{label:`this month · ${y(L.watchTime.monthSessions)} session${L.watchTime.monthSessions===1?"":"s"}`,value:We(L.watchTime.monthMs),icon:"calendar",tone:"info"},{label:"since Tracearr started recording",value:We(L.watchTime.totalMs),icon:"clock",tone:"note"},{label:"last watched",value:P(L.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:b!=null&&b.enabled?e.jsx(S,{tone:"ok",children:"enabled"}):e.jsx(S,{children:"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:i==="notifications",onClick:()=>void te("notifications",()=>q.put(`${l}/notifications`,b),"Notification settings saved.",()=>h(null)),children:"Save notifications"}),e.jsx($,{onClick:()=>{h(null),o()},children:"Discard changes"})]}),children:b?e.jsxs("div",{className:"checks columns",children:[e.jsx(Y,{label:"All notifications",hint:"The master switch. Turning this off hides every optional notification below.",checked:b.enabled,onChange:I=>h(W=>W&&{...W,enabled:I})}),e.jsx(Y,{label:"My Shows return dates",hint:"Remind this person when a followed show is about to return.",checked:b.showReturnAlerts,disabled:!b.enabled,onChange:I=>h(W=>W&&{...W,showReturnAlerts:I})}),e.jsx(Y,{label:"Sonarr television alerts",hint:"New episodes, additions and cancellation news supplied by Sonarr.",checked:b.sonarrAlerts,disabled:!b.enabled,onChange:I=>h(W=>W&&{...W,sonarrAlerts:I})}),e.jsx(Y,{label:"Radarr film alerts",hint:"Notify this person when Radarr imports a new film.",checked:b.radarrAlerts,disabled:!b.enabled,onChange:I=>h(W=>W&&{...W,radarrAlerts:I})}),e.jsx(Y,{label:"Optional app updates",hint:"Offer new app versions to this person. Mandatory compatibility updates are always enforced.",checked:b.updateAlerts,disabled:!b.enabled,onChange:I=>h(W=>W&&{...W,updateAlerts:I})}),e.jsx(Y,{label:"Library activity",hint:"Show alerts after the Memby library catalogue is refreshed.",checked:b.libraryAlerts,disabled:!b.enabled,onChange:I=>h(W=>W&&{...W,libraryAlerts:I})}),e.jsx(Y,{label:"Weekly watch-time summary",hint:"Send this person their week-to-date and month-to-date viewing on Sunday evening, and a summary of the month just gone once it ends. Needs Tracearr.",checked:b.watchTimeDigest,disabled:!b.enabled,onChange:I=>h(W=>W&&{...W,watchTimeDigest:I})}),e.jsx(Y,{label:"Service status",hint:"Memby deployment and Emby outage or recovery notices. Maintenance mode itself still applies.",checked:b.systemAlerts,disabled:!b.enabled,onChange:I=>h(W=>W&&{...W,systemAlerts:I})})]}):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:C.saved?e.jsxs(S,{tone:C.source==="admin"?"warn":"ok",children:["r",y(C.revision)," · ",C.source||"device"," · ",P(C.updatedAt)]}):e.jsx(S,{children:"defaults · never synced"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:i==="push",onClick:()=>void te("push",()=>q.put(`${l}/preferences`,{preferences:x??{}}),"Pushed to their televisions.",()=>j(null)),children:"Push to their televisions"}),e.jsx($,{onClick:()=>{j(null),o()},children:"Discard changes"}),e.jsx($,{onClick:()=>w({kind:"reset-preferences"}),children:"Restore defaults"}),e.jsx(J,{className:"crumb",to:`/admin/accounts/${encodeURIComponent(s)}/settings`,children:"History and rollback →"})]}),children:ne.map(I=>e.jsxs("div",{className:"group",children:[e.jsx("p",{className:"group-label",children:I.name}),I.definitions.map(W=>e.jsx(Pt,{definition:W,value:x==null?void 0:x[W.key],onChange:ue=>j(xe=>({...xe??{},[W.key]:ue}))},W.key))]},I.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:(L.themes??[]).length===0?e.jsx(S,{children:"all schemes"}):e.jsxs(S,{tone:"note",children:[y((L.themes??[]).length)," of ",y(se.length)]}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:i==="themes",onClick:()=>(u??[]).length===0?w({kind:"no-themes"}):void te("themes",()=>q.put(`${l}/themes`,{themes:u??[]}),"Colour schemes saved.",()=>v(null)),children:"Save colour schemes"}),e.jsx($,{onClick:()=>v(se.map(I=>I.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:se.map(I=>{var W,ue,xe;return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:(u??[]).includes(I.id),onChange:qe=>v(Ke=>qe.target.checked?[...Ke??[],I.id]:(Ke??[]).filter(M=>M!==I.id))}),e.jsx("span",{className:"switch"}),e.jsx("span",{className:"swatch",style:{"--swatch-surface":ys((W=I.palette)==null?void 0:W.surface),"--swatch-accent":ys((ue=I.palette)==null?void 0:ue.accent),"--swatch-hairline":ys((xe=I.palette)==null?void 0:xe.hairline)},children:e.jsx("i",{})}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:I.name}),e.jsx("p",{children:I.description})]})]},I.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:()=>w({kind:"remove-account"}),children:"Remove Memby access"})}),g?e.jsx(_t,{initial:g.name,busy:i==="rename",onCancel:()=>m(null),onConfirm:I=>void te("rename",()=>q.put(`${l}/devices/${encodeURIComponent(g.id)}`,{deviceName:I}),"Device renamed.",()=>m(null))}):null,R?e.jsx(Ut,{initial:R.name,renaming:R.id!==null,busy:i==="viewer-name",onCancel:()=>k(null),onConfirm:I=>void te("viewer-name",()=>R.id?q.put(`${l}/viewers/${encodeURIComponent(R.id)}`,{name:I}):q.post(`${l}/viewers`,{name:I}),R.id?"Viewer renamed.":"Viewer added.",()=>{k(null),E.reload()})}):null,p?e.jsx(Wt,{pending:p,busy:i,username:L.username,onCancel:()=>w(null),onConfirm:()=>{switch(p.kind){case"remove-device":return void te("remove-device",()=>q.del(`${l}/devices/${encodeURIComponent(p.deviceId)}`),"Device signed out.");case"remove-viewer":return void te("remove-viewer",()=>q.del(`${l}/viewers/${encodeURIComponent(p.viewer.id)}`),"Viewer removed.",()=>void E.reload());case"remove-account":return void te("remove-account",()=>q.del(`${l}/sessions`),"Memby access removed.",()=>n("/admin/accounts"));case"reset-recommendations":case"cancel-prompt":return void te("reset-rec",()=>q.del(`${l}/recommendations`),"Recommendation choices cleared.");case"reset-preferences":return void te("reset-prefs",()=>q.del(`${l}/preferences`),"Defaults restored.",()=>j(null));case"no-themes":return void te("themes",()=>q.put(`${l}/themes`,{themes:[]}),"Colour schemes saved.",()=>v(null))}}}):null]})}function Ot({prompt:s}){const n=s.ratings??[],t=[["Genres",s.genres],["Studios",s.studios],["Actors",s.actors],["Actresses",s.actresses],["Directors",s.directors],["Types",s.contentTypes]],i=[...n.map(a=>e.jsxs(de,{tone:"warn",children:[a.title," · ",y(a.rating)," ★"]},`r:${a.title}`)),...t.flatMap(([a,l])=>(l??[]).map(r=>e.jsxs(de,{children:[a,": ",r]},`${a}:${r}`)))];return i.length===0?e.jsx(ae,{children:"No recommendation selections have been saved."}):e.jsx("div",{className:"chips",children:i})}function Pt({definition:s,value:n,onChange:t}){if(s.kind==="toggle")return e.jsx(Y,{label:s.name,hint:s.description,checked:!!n,onChange:t});if(s.kind==="choice"||s.kind==="number"){const a=s.unit??"",l=s.kind==="number"?(s.numbers??[]).map(r=>({value:String(r),label:r===0?"No limit":a?`${r} ${a}`:String(r)})):s.options??[];return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("select",{value:String(n??""),onChange:r=>t(s.kind==="number"?Number(r.target.value):r.target.value),children:l.map(r=>e.jsx("option",{value:r.value,children:r.label},r.value))})})}if(s.kind==="multi"){const a=Array.isArray(n)?n:[],l=[...a,...(s.options??[]).map(r=>r.value).filter(r=>!a.includes(r))];return e.jsxs("div",{className:"field",children:[e.jsx("span",{children:s.name}),e.jsx("small",{children:s.description}),e.jsx("div",{className:"checks",children:l.map(r=>{const c=(s.options??[]).find(d=>d.value===r);return c?e.jsx(Y,{label:c.label,checked:a.includes(r),onChange:d=>t(d?[...a,r]:a.filter(o=>o!==r))},r):null})})]})}if(s.kind==="text")return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("input",{type:"text",value:String(n??""),maxLength:s.maxLength,placeholder:s.placeholder,onChange:a=>t(s.uppercase?a.target.value.toLocaleUpperCase("en-NZ"):a.target.value)})});const i=Array.isArray(n)?n:[];return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("textarea",{spellCheck:!1,placeholder:"One row id per line",value:i.join(` +`),onChange:a=>t(a.target.value.split(` +`).map(l=>l.trim()).filter(Boolean))})})}function _t({initial:s,busy:n,onConfirm:t,onCancel:i}){const[a,l]=f.useState(s||"Memby TV");return e.jsx("div",{className:"scrim",onPointerDown:r=>r.target===r.currentTarget&&i(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:"Name this device"}),e.jsx("p",{children:"The name a viewer sees in Settings → Devices, and what the console calls it."}),e.jsx(D,{label:"Device name",children:e.jsx("input",{type:"text",value:a,autoFocus:!0,maxLength:80,onChange:r=>l(r.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:i,children:"Cancel"}),e.jsx($,{variant:"primary",busy:n,disabled:!a.trim(),onClick:()=>t(a.trim()),children:"Rename"})]})]})})}function Ut({initial:s,renaming:n,busy:t,onConfirm:i,onCancel:a}){const[l,r]=f.useState(s);return e.jsx("div",{className:"scrim",onPointerDown:c=>c.target===c.currentTarget&&a(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:n?"Rename this viewer":"Add a viewer"}),e.jsx("p",{children:"The name shown on the television’s “Who’s watching?” screen. Everything they watch is kept by Memby and never reported to Emby."}),e.jsx(D,{label:"Name",children:e.jsx("input",{type:"text",value:l,autoFocus:!0,maxLength:40,onChange:c=>r(c.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:a,children:"Cancel"}),e.jsx($,{variant:"primary",busy:t,disabled:!l.trim(),onClick:()=>i(l.trim()),children:n?"Rename":"Add viewer"})]})]})})}function Wt({pending:s,busy:n,username:t,onConfirm:i,onCancel:a}){const r={"remove-device":{title:"Sign this device out of Memby?",body:"Its Emby account will not be changed. The set can sign in again at any time.",label:"Sign out",destructive:!0},"remove-account":{title:`Remove Memby access for ${t||"this user"}?`,body:"Every Memby device will be signed out. Their Emby account, viewing history and library permissions are untouched.",label:"Remove access",destructive:!0},"reset-recommendations":{title:"Clear this person's stored recommendation choices?",body:"Viewing history remains intact; only the explicit setup answers are removed.",label:"Clear",destructive:!0},"cancel-prompt":{title:"Cancel this person's queued recommendation prompt?",body:"They will not be invited to set up recommendations on their next launch.",label:"Cancel prompt",destructive:!1},"reset-preferences":{title:"Restore the Memby defaults for this person?",body:"Their televisions will pick the change up the next time they check in.",label:"Restore defaults",destructive:!0},"remove-viewer":{title:"Remove this viewer?",body:"What they were part-way through, what they had watched and their favourites are deleted, on every television in the house. The Emby account is untouched.",label:"Remove viewer",destructive:!0},"no-themes":{title:"Allow this person no colour schemes?",body:"They will be left on Midnight with nothing to choose between.",label:"Save anyway",destructive:!0}}[s.kind];return e.jsx(Se,{title:r.title,body:r.body,confirmLabel:r.label,destructive:r.destructive,busy:!!n,onConfirm:i,onCancel:a})}function Bt(s,n){if(s.kind==="toggle")return n?"On":"Off";if(s.kind==="choice"){const i=(s.options??[]).find(a=>a.value===n);return i?i.label:String(n??"")}if(s.kind==="number")return Number(n)===0&&s.unit?"No limit":s.unit?`${n} ${s.unit}`:String(n??"");const t=Array.isArray(n)?n:[];return t.length===0?"None":t.map(i=>{var a;return((a=(s.options??[]).find(l=>l.value===i))==null?void 0:a.label)??i}).join(", ")}function Vt(){const{userId:s=""}=He(),{wrap:n}=le(),{busy:t,run:i}=ie(),a=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:l,error:r,loading:c,reload:d}=Q(`${a}/preferences/history`,{pollMs:3e4}),[o,x]=f.useState(new Set),[j,u]=f.useState(null),v=(l==null?void 0:l.username)||"this account",b=(l==null?void 0:l.devices)??[],h=(l==null?void 0:l.revisions)??[],p=(l==null?void 0:l.catalogue)??[],w=m=>x(R=>{const k=new Set(R);return k.has(m)?k.delete(m):k.add(m),k}),g=m=>i("restore",async()=>{await n(()=>q.post(`${a}/preferences/revisions/${m}/restore`),`Restored r${m}.`),u(null),await d()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Settings history",intro:`Every change to ${v}'s synced settings, and which of their televisions has taken it.`,crumbs:e.jsxs(J,{to:`/admin/accounts/${encodeURIComponent(s)}`,children:["← ",v]})}),e.jsx(U,{message:r}),c?e.jsx(H,{}):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:l!=null&&l.saved?e.jsxs(S,{tone:l.currentSource==="admin"?"warn":"ok",children:["now on r",y(l.currentRevision)," · ",l.currentSource||"device"]}):e.jsx(S,{children:"defaults · never synced"}),children:b.length===0?e.jsx(ae,{children:"No television has been signed in to this account."}):e.jsx("div",{className:"list",children:b.map(m=>{const R=m.never?void 0:m.behind?"bad":"ok";return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[e.jsx("span",{className:"dot-state","data-tone":R})," ",m.name||"Memby TV"," ",m.signedOut?e.jsx(S,{children:"signed out"}):null]}),e.jsxs("p",{children:[m.never?"Has not fetched these settings yet":`Holding r${y(m.revision)} · taken ${P(m.ackedAt)}`,m.clientVersion?` · Memby ${m.clientVersion}`:"",m.signedOut?"":` · last seen ${P(m.lastSeen)}`]})]}),e.jsx("div",{className:"list-actions",children:m.never?e.jsx(S,{children:"never taken one"}):m.behind?e.jsxs(S,{tone:"bad",children:[y(m.behind)," behind"]}):e.jsx(S,{tone:"ok",children:"up to date"})})]},m.deviceId||m.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(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(X,{columns:6,children:"Nothing has been changed on this account yet."}):h.flatMap(m=>{const R=o.has(m.revision),k=m.acks??[],E=m.changes??[],L=[e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(m.createdAt)}),e.jsxs("td",{className:"num nowrap",children:["r",y(m.revision)," ",m.current?e.jsx(S,{tone:"ok",children:"current"}):null]}),e.jsxs("td",{className:"nowrap",children:[e.jsx(S,{tone:m.source==="admin"?"warn":"ok",children:m.author}),m.restoredFrom?e.jsxs("span",{className:"muted",children:[" restored r",y(m.restoredFrom)]}):null]}),e.jsx("td",{className:"muted",children:m.initial?e.jsx("span",{className:"muted",children:"First recorded settings"}):E.length===0?e.jsx("span",{className:"muted",children:"No visible change"}):e.jsx("div",{className:"chips",children:E.map((A,N)=>e.jsxs(de,{children:[A.name,": ",A.before," → ",A.after]},`${A.name}:${N}`))})}),e.jsx("td",{className:"num",children:k.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{title:k.map(A=>A.deviceName||A.deviceId).join(", "),children:y(k.length)})}),e.jsx("td",{className:"num nowrap",children:e.jsxs("span",{className:"list-actions",children:[e.jsx($,{size:"sm",onClick:()=>w(m.revision),children:R?"Hide":"Show"}),m.current?null:e.jsx($,{size:"sm",onClick:()=>u(m.revision),children:"Restore"})]})})]},m.revision)];return R&&L.push(e.jsx("tr",{children:e.jsx("td",{colSpan:6,className:"muted",children:e.jsx("div",{className:"chips",children:p.map(A=>{var N;return e.jsxs(de,{children:[A.name,":"," ",Bt(A,(N=m.preferences)==null?void 0:N[A.key])]},A.key)})})})},`${m.revision}:detail`)),L})})]})})})]}),j!==null?e.jsx(Se,{title:`Restore revision ${j}?`,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 g(j),onCancel:()=>u(null)}):null]})}function Ht({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(de,{tone:t.version===s.version?"ok":void 0,children:t.version},t.version))})}function zt(){const{status:s,error:n,loading:t}=ve(),i=(s==null?void 0:s.clients)??[],a=i.filter(r=>(r.capabilities??[]).includes("server_features_v1")),l=new Set(i.map(r=>r.version).filter(Boolean));return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands."}),e.jsx(U,{message:n}),t?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"devices known",value:y(i.length),icon:"tv",tone:"info"},{label:"active in the last quarter hour",value:y(i.filter(r=>ps(r.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"reporting their capabilities",value:y(a.length),icon:"sliders",tone:"ok"},{label:"app builds in service",value:y(l.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(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(X,{columns:6,children:"No devices have signed in yet."}):i.map(r=>{const c=(r.capabilities??[]).includes("server_features_v1"),d=As(r.lastSeen);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":d.tone,title:d.label}),e.jsx(J,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||"Memby TV"})]})}),e.jsx("td",{className:"muted",children:r.username}),e.jsx("td",{className:"mono",children:r.version||"legacy"}),e.jsx("td",{children:e.jsx(Ht,{client:r})}),e.jsx("td",{children:e.jsx(S,{tone:c?"ok":"warn",children:c?"reported":"missing"})}),e.jsx("td",{className:"nowrap muted",children:P(r.lastSeen)})]},`${r.deviceId}:${r.username}`)})})]})})})]})]})}const Ys={user:"",q:"",ip:"",outcome:"",from:"",to:""},Kt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Gt(){var m,R,k,E,L,A;const[s,n]=f.useState("log"),[t,i]=f.useState(7),[a,l]=f.useState(Ys),[r,c]=f.useState(0),d=100,o=f.useMemo(()=>ze({...a,days:a.from?void 0:t||void 0,limit:d,offset:r*d}),[a,t,r]),x=Q(`/admin/api/logins${o}`,{enabled:s==="log"}),j=Q(`/admin/api/logins/devices${o}`,{enabled:s==="devices"}),u=((m=x.data)==null?void 0:m.users)??((R=j.data)==null?void 0:R.users)??[],v=((k=x.data)==null?void 0:k.totals)??((E=j.data)==null?void 0:E.totals),b=((L=x.data)==null?void 0:L.retentionDays)??((A=j.data)==null?void 0:A.retentionDays)??90,h=s==="log"?x.loading:j.loading,p=s==="log"?x.error:j.error,w=N=>{l(_=>({..._,...N})),c(0)},g=Object.entries(a).some(([,N])=>N!=="")||!!a.from;return e.jsxs(e.Fragment,{children:[e.jsx(B,{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(U,{message:p}),v?e.jsx(ce,{tiles:[{label:"Successful sign-ins",value:y(v.logins),icon:"key",tone:"ok"},{label:"Refused",value:y(v.failures),icon:"shield",tone:v.failures>0?"warn":void 0},{label:"Televisions",value:y(v.devices),icon:"tv",tone:"info"},{label:"People",value:y(v.users),icon:"people",tone:"note"},{label:"Addresses",value:y(v.addresses),icon:"globe",tone:"data"},{label:"History kept",value:`${b} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Be,{value:a.from?-1:t,options:Kt.map(N=>({value:N.value,label:N.label})),onChange:N=>{i(N),w({from:"",to:""})}})}),e.jsx(D,{label:"Person",children:e.jsxs("select",{value:a.user,onChange:N=>w({user:N.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),u.map(N=>e.jsx("option",{value:N.id,children:N.username||N.id},N.id))]})}),e.jsx(D,{label:"Outcome",children:e.jsxs("select",{value:a.outcome,onChange:N=>w({outcome:N.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:N=>w({ip:N.target.value})})}),e.jsx(D,{label:"From",children:e.jsx("input",{type:"date",value:a.from,onChange:N=>w({from:N.target.value})})}),e.jsx(D,{label:"To",children:e.jsx("input",{type:"date",value:a.to,onChange:N=>w({to:N.target.value})})}),e.jsx(D,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:a.q,placeholder:"Name, device or address",onChange:N=>w({q:N.target.value})})}),e.jsx("div",{className:"filter-actions",children:g?e.jsx($,{variant:"quiet",size:"sm",onClick:()=>{l(Ys),c(0)},children:"Clear"}):null})]}),h?e.jsx(H,{}):s==="log"?e.jsx(Zt,{data:x.data,page:r,limit:d,onPage:c}):e.jsx(Yt,{data:j.data})]})}function Zt({data:s,page:n,limit:t,onPage:i}){if(!s)return null;const a=s.events.length,l=s.total===0?0:n*t+1;return e.jsxs(e.Fragment,{children:[e.jsxs(pe,{cols:"wide",children:[e.jsx(T,{title:"Attempts per day",intro:"Grouped in the server's own timezone, so an evening sign-in stays on the day it happened.",icon:"chart",tone:"info",children:e.jsx(xs,{data:s.days,labelOf:r=>r.day,valueOf:r=>r.logins+r.failures,toneOf:r=>r.failures>r.logins?"bad":void 0,title:r=>`${r.day}: ${r.logins} in, ${r.failures} refused, ${r.devices} televisions`})}),e.jsx(T,{title:"Where from",icon:"globe",tone:"data",children:s.addresses.length===0?e.jsx(ae,{children:"No addresses in this window."}):e.jsx("div",{className:"list",children:s.addresses.slice(0,8).map(r=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:r.ipAddress}),e.jsxs("p",{children:[y(r.logins)," in",r.failures>0?` · ${y(r.failures)} refused`:""]})]}),r.failures>0&&r.logins===0?e.jsx(S,{tone:"bad",children:"only refused"}):null]},r.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":`${y(l)}–${y(l+a-1)} of ${y(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(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(X,{columns:6,children:"No sign-in attempts match these filters."}):s.events.map(r=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(r.occurredAt)}),e.jsx("td",{children:r.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{children:r.deviceId?e.jsx(J,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||r.deviceId}):e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"mono nowrap",children:r.ipAddress||"—"}),e.jsx("td",{className:"mono",children:r.clientVersion||"—"}),e.jsx("td",{className:"nowrap",children:r.success?r.newDevice?e.jsx(S,{tone:"info",children:"first sign-in"}):e.jsx(S,{tone:"ok",children:"got in"}):e.jsx(S,{tone:"bad",children:r.failureReason||"refused"})})]},r.id))})]})})})]})}function Yt({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(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(X,{columns:9,children:"No television has connected in this window."}):s.devices.map(n=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(J,{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?y(n.loginsToday):"—"}),e.jsx("td",{className:"num",children:y(n.logins)}),e.jsx("td",{className:"num",children:n.failures>0?e.jsx("span",{className:"mono",children:y(n.failures)}):"—"}),e.jsx("td",{className:"num",children:y(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 Jt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Qt(){const{deviceId:s=""}=He(),[n,t]=f.useState(7),i=f.useMemo(()=>`/admin/api/logins/devices/${encodeURIComponent(s)}${ze({days:n||void 0,limit:200})}`,[s,n]),{data:a,error:l,loading:r}=Q(i,{enabled:!!s}),c=a==null?void 0:a.summary,d=(c==null?void 0:c.deviceName)||s;return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:d,intro:"One television's whole relationship with the gateway.",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(J,{to:"/admin/clients",children:"Devices"}),e.jsx("span",{children:"/"}),e.jsx(J,{to:"/admin/logins",children:"Sign-ins"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:d})]}),actions:e.jsx(Be,{value:n,options:Jt.map(o=>({value:o.value,label:o.label})),onChange:t})}),e.jsx(U,{message:l}),r?e.jsx(H,{}):a?e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"Sign-ins today",value:y((c==null?void 0:c.loginsToday)??0),icon:"clock",tone:"ok"},{label:"Sign-ins in total",value:y((c==null?void 0:c.logins)??0),icon:"key",tone:"info"},{label:"Refused",value:y((c==null?void 0:c.failures)??0),icon:"shield",tone:((c==null?void 0:c.failures)??0)>0?"warn":void 0},{label:"Addresses seen",value:y((c==null?void 0:c.distinctIps)??0),icon:"globe",tone:"data"},{label:"First seen",value:c!=null&&c.firstLogin?P(c.firstLogin):"—",small:!0,icon:"history"},{label:"Last seen",value:c!=null&&c.lastLogin?P(c.lastLogin):"—",small:!0,icon:"pulse",tone:"note"}]}),e.jsxs(pe,{cols:"wide",children:[e.jsx(T,{title:"Connections per day",icon:"chart",tone:"info",children:e.jsx(xs,{data:a.days,labelOf:o=>o.day,valueOf:o=>o.logins+o.failures,toneOf:o=>o.failures>o.logins?"bad":void 0,title:o=>`${o.day}: ${o.logins} in${o.failures?`, ${o.failures} refused`:""}`})}),e.jsxs("div",{className:"stack",children:[e.jsx(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:(c==null?void 0:c.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:(c==null?void 0:c.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:a.versions.length===0?e.jsx(ae,{children:"No build history for this television."}):e.jsx("div",{className:"list",children:a.versions.map(o=>e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:o.version}),e.jsxs("p",{children:[P(o.firstSeen)," → ",P(o.lastSeen)]})]})},o.version))})})]})]}),e.jsx(T,{title:"Addresses",icon:"globe",tone:"data",children:a.addresses.length===0?e.jsx(ae,{children:"No addresses recorded in this window."}):e.jsx("div",{className:"chips",children:a.addresses.map(o=>e.jsxs(de,{tone:o.failures>0?"warn":"data",children:[o.ipAddress," · ",y(o.logins),o.failures>0?` (+${y(o.failures)} refused)`:""]},o.ipAddress))})}),e.jsx(T,{title:"Every attempt",icon:"key",tone:"ok",actions:e.jsxs("span",{className:"filter-summary",children:[y(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(X,{columns:6,children:"This television has not connected in the selected window."}):a.events.map(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(o.occurredAt)}),e.jsx("td",{children:o.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{className:"mono nowrap",children:o.ipAddress||"—"}),e.jsx("td",{className:"mono",children:o.clientVersion||"—"}),e.jsx("td",{className:"muted",children:o.method}),e.jsx("td",{className:"nowrap",children:o.success?o.newDevice?e.jsx(S,{tone:"info",children:"first sign-in"}):e.jsx(S,{tone:"ok",children:"got in"}):e.jsx(S,{tone:"bad",children:o.failureReason||"refused"})})]},o.id))})]})})})]}):null]})}function Xt(){const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(!1),o=(s==null?void 0:s.library.byType)??{},x=!!(s!=null&&s.syncRunning),j=u=>r(u,async()=>{await a(()=>q.post("/admin/api/sync",{kind:u}),u==="full"?"Full re-import started.":"Import started."),d(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Library",intro:"Import and inspect the catalogue Memby ranks."}),e.jsx(U,{message:n}),t||!s?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"items",value:y(s.library.total),icon:"library",tone:"data"},...Object.keys(o).sort().map(u=>({label:u,value:y(o[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:x?"Import running…":`An incremental import runs automatically every ${s.syncEvery}.`}),children:e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",icon:"sync",disabled:x,busy:l==="incremental",onClick:()=>void j("incremental"),children:"Sync new items"}),e.jsx($,{icon:"database",disabled:x,onClick:()=>d(!0),children:"Full re-import"})]})})]}),c?e.jsx(Se,{title:"Re-import the entire library?",body:"A full pass mark-and-sweeps the catalogue and can take several minutes on a large library. Televisions keep reading the current table throughout.",confirmLabel:"Re-import",busy:l==="full",onConfirm:()=>void j("full"),onCancel:()=>d(!1)}):null]})}function ea(){var j;const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),c=(s==null?void 0:s.requestUsers)??[],d=((j=s==null?void 0:s.requestPolicy)==null?void 0:j.allowedUserIds)??[],o=f.useMemo(()=>new Map(((s==null?void 0:s.requestUsage)??[]).map(u=>[u.userId,u])),[s==null?void 0:s.requestUsage]),x=u=>r(`access-${u}`,async()=>{const v=d.includes(u)?d.filter(b=>b!==u):[...d,u];await a(()=>q.post("/admin/api/request-policy",{allowedUserIds:v}),v.includes(u)?"Request access granted.":"Request access removed."),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Media requests",intro:"Who can ask for something the library does not have."}),e.jsx(U,{message:n}),t?e.jsx(H,{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(S,{tone:s!=null&&s.radarrReady?"ok":"bad",children:["Movies ",s!=null&&s.radarrReady?"ready":"not configured"]}),e.jsxs(S,{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(ae,{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:c.length===0?e.jsx(ae,{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:c.map(u=>{const v=o.get(u.id),b=d.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:(v==null?void 0:v.requests)??0}),e.jsx("td",{className:"muted nowrap",children:v!=null&&v.lastRequest?P(v.lastRequest):"—"}),e.jsx("td",{children:e.jsx($,{size:"sm",variant:b?"quiet":"primary",busy:l===`access-${u.id}`,onClick:()=>void x(u.id),children:b?"Remove access":"Give access"})})]},u.id)})})]})})})]})]})}function sa(){const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(!1),o=s==null?void 0:s.forYou,x=!!(s!=null&&s.forYouRunning),j=(u,v,b)=>r(v,async()=>{await a(()=>q.post("/admin/api/for-you",{action:u}),b),d(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"For You",intro:"The prepared pools personalised rows are drawn from."}),e.jsx(U,{message:n}),t?e.jsx(H,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"Tracearr sessions",value:y((o==null?void 0:o.tracearrSessions)??0),icon:"play",tone:"info"},{label:"user profiles",value:y((o==null?void 0:o.profiles)??0),icon:"people",tone:"note"},{label:"ranked candidates",value:y((o==null?void 0:o.candidates)??0),icon:"sparkle",tone:"note"},{label:"last full import",value:P(o==null?void 0:o.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:x?"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:x,busy:l==="import",onClick:()=>void j("incremental-import","import","Import started."),children:"Import recent sessions"}),e.jsx($,{icon:"database",disabled:x,onClick:()=>d(!0),children:"Full Tracearr backfill"}),e.jsx($,{icon:"sync",disabled:x,busy:l==="rebuild",onClick:()=>void j("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(J,{to:"/admin/inspector",children:"Open the inspector"}),children:e.jsx(e.Fragment,{})})]}),c?e.jsx(Se,{title:"Backfill all Tracearr history?",body:"Every session is re-read and every active user's pool is rebuilt. It is safe at any time — televisions keep reading the last finished pool — but on a long history it takes a while.",confirmLabel:"Backfill",busy:l==="full",onConfirm:()=>void j("full-import","full","Backfill started."),onCancel:()=>d(!1)}):null]})}const na=[["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 ta(s){return s?na.flatMap(([n,t])=>Object.entries(s[t]??{}).map(([i,a])=>({dimension:n,name:i,weight:a.weight??0,evidence:a.evidence??0}))).sort((n,t)=>Math.abs(t.weight)-Math.abs(n.weight)):[]}const Js=s=>`${s>=0?"+":""}${s.toFixed(3)}`;function aa(){const{status:s}=ve(),{wrap:n}=le(),{busy:t,run:i}=ie(),[a,l]=f.useState(""),[r,c]=f.useState("default"),[d,o]=f.useState("0"),[x,j]=f.useState(""),[u,v]=f.useState(null),[b,h]=f.useState("Choose a person to inspect their recommendations."),[p,w]=f.useState(""),g=(s==null?void 0:s.requestUsers)??[],m=()=>i("run",async()=>{if(!a){w("Choose a person to pressure-test.");return}w(""),h("Running the permission check and the scorer…");const A=new URLSearchParams({userId:a,context:r,minutes:d||"0",limit:"100"});x&&A.set("at",new Date(x).toISOString());const N=await n(()=>q.get(`/admin/api/recommendations?${A.toString()}`));N?(v(N),h(`Scored at ${new Date().toLocaleTimeString()}.`)):h("Pressure test failed.")}),R=ta((u==null?void 0:u.profile)??null).slice(0,24),k=(u==null?void 0:u.actions)??[],E=(u==null?void 0:u.items)??[],L=(u==null?void 0:u.profileMeta)??{};return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Score inspector",intro:"Re-run the ranker for one person and read every component."}),e.jsx(U,{message:p}),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 m(),children:"Run pressure test"}),e.jsx("span",{className:"hint",children:b})]}),children:e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Person",children:e.jsxs("select",{value:a,onChange:A=>l(A.target.value),children:[e.jsx("option",{value:"",children:"Choose a person…"}),g.map(A=>e.jsx("option",{value:A.id,children:A.username},A.id))]})}),e.jsx(D,{label:"Context",children:e.jsxs("select",{value:r,onChange:A=>c(A.target.value),children:[e.jsx("option",{value:"default",children:"Default"}),e.jsx("option",{value:"bedtime",children:"One episode before bed"}),e.jsx("option",{value:"hidden",children:"Hidden library"}),e.jsx("option",{value:"new-releases",children:"Recent new releases"})]})}),e.jsx(D,{label:"Available minutes",children:e.jsx("input",{type:"number",min:0,max:360,value:d,onChange:A=>o(A.target.value)})}),e.jsx(D,{label:"Evaluate at",children:e.jsx("input",{type:"datetime-local",value:x,onChange:A=>j(A.target.value)})})]})}),u?e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"prepared pool",value:y(u.poolCandidates),icon:"database",tone:"data"},{label:"permission eligible",value:y(u.permissionEligible),icon:"shield",tone:"ok"},{label:"ranked result",value:y(E.length),icon:"sparkle",tone:"note"},{label:"source events",value:y(L.sourceEvents??0),icon:"pulse",tone:"info"},{label:"algorithm",value:L.algorithmVersion||"—",small:!0,icon:"chip"},{label:"pool built",value:P(L.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:[R.length===0?e.jsx(ae,{children:"No repeated affinity evidence yet; cold-start priors apply."}):e.jsx("div",{className:"chips",children:R.map(A=>e.jsxs(de,{tone:A.weight<0?"bad":void 0,children:[A.dimension,": ",A.name," ",Js(A.weight)," · n=",y(A.evidence)]},`${A.dimension}:${A.name}`))}),k.length===0?e.jsx(ae,{children:"No explicit recommendation actions."}):e.jsx("div",{className:"chips",children:k.map((A,N)=>e.jsxs(de,{tone:"ok",children:[A.action,": ",A.title||A.itemId]},`${A.action}:${N}`))})]}),E.length===0?e.jsx(T,{children:e.jsx(ae,{children:"No candidates survived this context, the explicit exclusions and the permission filter."})}):e.jsx(pe,{children:E.map((A,N)=>{const _=A.explanation??{},se=Object.entries(_.components??{}).sort((O,re)=>Math.abs(re[1])-Math.abs(O[1])),ne=A.exposure??{},te=[A.type,A.year,A.runtimeMinutes?`${A.runtimeMinutes} min`:null,...A.genres??[]].filter(Boolean).join(" · ");return e.jsxs(T,{title:`#${N+1} · ${A.title}`,intro:te,actions:e.jsx(de,{tone:"ok",children:Number(_.total??0).toFixed(3)}),children:[e.jsxs("p",{className:"hint",children:[A.preparedReason||"No legacy prepared explanation",A.compatibilityLabel?` · ${A.compatibilityLabel}`:""]}),e.jsxs("div",{className:"chips",children:[(_.reasonCodes??[]).map(O=>e.jsx(de,{tone:"ok",children:O},O)),se.map(([O,re])=>e.jsxs(de,{tone:re<0?"bad":void 0,children:[O,"=",Js(re)]},O))]}),e.jsxs("details",{children:[e.jsx("summary",{className:"muted",children:"Pool, row and exposure detail"}),e.jsxs("p",{className:"hint",children:["Base rank ",y(A.baseRank??0)," · base ",Number(A.baseScore??0).toFixed(3)," · affinity ",Number(A.affinityScore??0).toFixed(3)," · compatibility"," ",Number(A.compatibilityScore??0).toFixed(3)," · impressions"," ",y(ne.impressions??0)," · focuses ",y(ne.focuses??0)," · selects"," ",y(ne.selects??0)]}),e.jsx("div",{className:"chips",children:(A.eligibleRows??[]).map(O=>e.jsx(de,{tone:"ok",children:O},O))}),A.preparedEvidenceTitle?e.jsxs("p",{className:"hint",children:["Prepared evidence: ",A.preparedEvidenceTitle]}):null]})]},`${N}:${A.title}`)})})]}):null]})}const ia=4,De=[{id:"home",label:"Home",type:"films and television shows"},{id:"movies",label:"Movies",type:"films"},{id:"tv_shows",label:"TV Shows",type:"television shows"}],Ge=()=>({pinnedItems:[],primeSubtitle:""}),Cs=[{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 Qs(s){const n=s.getTimezoneOffset()*6e4;return new Date(s.getTime()-n).toISOString().slice(0,16)}function Xs(s){return!!(s&&Number.isFinite(new Date(s).getTime())&&new Date(s).getFullYear()>=2e3)}function ra(s){return s.frequency??"once"}function la(s){if(s.frequency==="daily")return`Every day · ${s.startTime}–${s.endTime}`;if(s.frequency==="weekly"){const n=Cs.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 en(s,n){const t=new Date;t.setMinutes(Math.ceil(t.getMinutes()/30)*30,0,0);const i=new Date(t.getTime()+2*60*60*1e3),a=n==="home"||n==="movies"&&s.type==="Movie"||n==="tv_shows"&&s.type==="Series";return{id:crypto.randomUUID(),itemId:s.id,startAt:t.toISOString(),endAt:i.toISOString(),priority:0,enabled:!0,placements:[a?n:"home"]}}function oa(){var te,O,re;const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a,show:l}=le(),{busy:r,run:c}=ie(),[d,o]=f.useState("home"),[x,j]=f.useState({home:Ge(),movies:Ge(),tv_shows:Ge()}),[u,v]=f.useState(!1),[b,h]=f.useState(""),[p,w]=f.useState(null),[g,m]=f.useState([]),[R,k]=f.useState(null),E=s==null?void 0:s.heroPolicy;f.useEffect(()=>{var C,F,Z;u||!E||(j({home:((C=E.placements)==null?void 0:C.home)??{pinnedItems:E.pinnedItems??[],primeSubtitle:E.primeSubtitle??""},movies:((F=E.placements)==null?void 0:F.movies)??Ge(),tv_shows:((Z=E.placements)==null?void 0:Z.tv_shows)??Ge()}),m(E.schedules??[]))},[E,u]);const L=()=>c("search",async()=>{const C=b.trim();if(!C)return;const F=await a(()=>q.get(`/admin/api/hero/search?q=${encodeURIComponent(C)}`));F&&w(F.items??[])}),A=x[d],N=A.pinnedItems??[],_=C=>{j(F=>({...F,[d]:{...F[d],...C}})),v(!0)},se=C=>{if(!N.some(F=>F.id===C.id)){if(N.length>=ia){l("Remove a pinned title before adding another.","bad");return}_({pinnedItems:[...N,C]})}},ne=()=>c("save",async()=>{await a(()=>q.post("/admin/api/hero-policy",{placements:Object.fromEntries(Object.entries(x).map(([C,F])=>[C,{pinnedItemIds:(F.pinnedItems??[]).map(Z=>Z.id),primeSubtitle:F.primeSubtitle.trim()}])),schedules:g}),"Hero saved."),v(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Featured content",intro:"Manage an independent, backend-resolved hero for Home, Movies and TV Shows."}),e.jsx(U,{message:n}),t?e.jsx(H,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(pe,{children:De.map(C=>{var Ie;const F=x[C.id],Z=g.filter(fe=>fe.enabled&&(fe.placements??["home"]).includes(C.id)).sort((fe,I)=>I.priority-fe.priority)[0],he=(F.pinnedItems??[]).length?"Manual":Z?"Schedule ready":"Automatic",Ne=(Ie=E==null?void 0:E.items)==null?void 0:Ie.find(fe=>fe.id===(Z==null?void 0:Z.itemId)),Ae=(F.pinnedItems??[]).map(fe=>fe.name).join(", ")||(Ne==null?void 0:Ne.name)||(Z==null?void 0:Z.itemId)||"Resolved for each viewer";return e.jsx(T,{title:C.label,intro:`${he} · ${Ae}`,tone:C.id===d?"info":void 0,children:e.jsxs($,{size:"sm",variant:"quiet",onClick:()=>o(C.id),children:["Manage ",C.label]})},C.id)})}),e.jsx("div",{className:"tabs",role:"tablist","aria-label":"Hero placement",children:De.map(C=>e.jsx($,{variant:d===C.id?"primary":"quiet",onClick:()=>o(C.id),children:C.label},C.id))}),e.jsxs(T,{title:`${(te=De.find(C=>C.id===d))==null?void 0:te.label} hero`,intro:`Pinned ${(O=De.find(C=>C.id===d))==null?void 0:O.type} lead this section only. Empty places use this placement’s automatic selection.`,icon:"star",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:r==="save",onClick:()=>void ne(),children:"Save hero"}),e.jsx($,{onClick:()=>{_({pinnedItems:[]})},children:"Clear pins"}),u?e.jsx("span",{className:"hint",children:"Unsaved changes."}):null]}),children:[N.length===0?e.jsx(ae,{children:"No titles are pinned. The hero is entirely release-aware and automatic."}):e.jsx("div",{className:"hero-pins",children:N.map((C,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:C.name}),e.jsxs("small",{children:[C.type,C.year?` · ${C.year}`:""]})]}),e.jsx($,{size:"sm",icon:"clock",onClick:()=>k(en(C,d)),children:"Schedule"}),e.jsx($,{variant:"quiet",size:"sm",icon:"close",title:`Remove ${C.name}`,onClick:()=>_({pinnedItems:N.filter(Z=>Z.id!==C.id)})})]},C.id))}),e.jsx(D,{label:"Prime-card subtitle",hint:"Optional wording under the large first card. Leave blank to use Memby's natural release or rating reason.",children:e.jsx("input",{type:"text",maxLength:160,value:A.primeSubtitle,placeholder:"Leave blank for the automatic reason",onChange:C=>{_({primeSubtitle:C.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(S,{tone:"info",children:(E==null?void 0:E.timeZone)||"server local time"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:r==="save",onClick:()=>void ne(),children:"Save schedule"}),e.jsx("span",{className:"hint",children:"Daily and weekly rules repeat until you switch them off."})]}),children:g.length===0?e.jsx(ae,{children:"No scheduled heroes yet. Use Schedule beside a pinned or searched title."}):e.jsx("div",{className:"hero-schedule-list",children:[...g].sort((C,F)=>Number(F.enabled)-Number(C.enabled)||F.priority-C.priority).map(C=>{const F=[...(E==null?void 0:E.items)??[],...N,...p??[]].find(Z=>Z.id===C.itemId);return e.jsxs("article",{className:"hero-schedule","data-enabled":C.enabled||void 0,children:[e.jsxs("div",{className:"hero-schedule-time",children:[e.jsx("b",{children:C.frequency==="weekly"?"Weekly":C.frequency==="daily"?"Daily":"Once"}),e.jsx("span",{children:C.frequency?C.startTime:C.startAt?new Date(C.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)??C.itemId}),e.jsx(S,{tone:C.enabled?"ok":void 0,children:C.enabled?"enabled":"paused"})]}),e.jsx("p",{children:la(C)}),e.jsxs("div",{className:"chips",children:[(C.placements??["home"]).map(Z=>{var he;return e.jsx("span",{className:"chip",children:(he=De.find(Ne=>Ne.id===Z))==null?void 0:he.label},Z)}),C.priority!==0?e.jsxs("span",{className:"chip",children:["Priority ",C.priority]}):null]})]}),e.jsxs("div",{className:"hero-schedule-actions",children:[e.jsx($,{size:"sm",onClick:()=>k({...C}),children:"Edit"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{m(Z=>Z.map(he=>he.id===C.id?{...he,enabled:!he.enabled}:he)),v(!0)},children:C.enabled?"Pause":"Enable"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{m(Z=>Z.filter(he=>he.id!==C.id)),v(!0)},children:"Remove"})]})]},C.id)})})}),e.jsxs(T,{title:"Find a title",intro:`Search the imported Emby catalogue. Add a result to the selected ${(re=De.find(C=>C.id===d))==null?void 0:re.label} placement; switch tabs to show it in more than one section.`,icon:"search",tone:"info",children:[e.jsxs("div",{className:"field-row",children:[e.jsx(D,{label:"Title",grow:!0,children:e.jsx("input",{type:"search",value:b,placeholder:"Search films and television shows",onChange:C=>h(C.target.value),onKeyDown:C=>{C.key==="Enter"&&L()}})}),e.jsx($,{busy:r==="search",icon:"search",onClick:()=>void L(),children:"Search"})]}),p===null?null:p.length===0?e.jsx(ae,{children:"No playable films or series matched that search."}):e.jsx(pe,{children:p.map(C=>e.jsx(T,{title:C.name,intro:`${C.type||"Title"} · ${C.year||"Year unknown"}`,children:e.jsxs("div",{className:"row",children:[e.jsx($,{size:"sm",icon:"plus",disabled:N.some(F=>F.id===C.id)||d==="movies"&&C.type!=="Movie"||d==="tv_shows"&&C.type!=="Series",onClick:()=>se(C),children:N.some(F=>F.id===C.id)?"Pinned":"Add to hero"}),e.jsx($,{size:"sm",icon:"clock",onClick:()=>k(en(C,d)),children:"Schedule"})]})},C.id))})]})]}),R?e.jsx(ca,{schedule:R,item:[...(E==null?void 0:E.items)??[],...N,...p??[]].find(C=>C.id===R.itemId),timeZone:(E==null?void 0:E.timeZone)||"server local time",isNew:!g.some(C=>C.id===R.id),onCancel:()=>k(null),onSave:C=>{m(F=>F.some(Z=>Z.id===C.id)?F.map(Z=>Z.id===C.id?C:Z):[...F,C]),k(null),v(!0)}}):null]})}function ca({schedule:s,item:n,timeZone:t,isNew:i,onSave:a,onCancel:l}){const[r,c]=f.useState({...s,weekdays:[...s.weekdays??[]]}),d=ra(r),o=h=>{const p=new Date,w=new Date(p.getTime()+2*60*60*1e3);c(g=>{var m;return h==="once"?{...g,frequency:void 0,startAt:Xs(g.startAt)?g.startAt:p.toISOString(),endAt:Xs(g.endAt)?g.endAt:w.toISOString()}:{...g,frequency:h,startTime:g.startTime||"18:00",endTime:g.endTime||"22:00",weekdays:h==="weekly"?(m=g.weekdays)!=null&&m.length?g.weekdays:[1,2,3,4,5]:[]}})},x=r.placements??["home"],j=h=>h==="home"||h==="movies"&&(n==null?void 0:n.type)==="Movie"||h==="tv_shows"&&(n==null?void 0:n.type)==="Series",u=!!(r.startAt&&r.endAt&&new Date(r.endAt)>new Date(r.startAt)),v=!!(r.startTime&&r.endTime&&r.startTime!==r.endTime&&(d!=="weekly"||(r.weekdays??[]).length>0)),b=d==="once"?u:v;return e.jsx("div",{className:"scrim",onPointerDown:h=>h.target===h.currentTarget&&l(),children:e.jsxs("div",{className:"dialog hero-schedule-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"hero-schedule-title",children:[e.jsxs("div",{className:"hero-schedule-dialog-head",children:[e.jsx("span",{className:"hero-schedule-kicker",children:"Hero schedule"}),e.jsx("h2",{id:"hero-schedule-title",children:(n==null?void 0:n.name)??r.itemId}),e.jsxs("p",{children:["Choose exactly when this title can lead the selected sections. Times use ",t,"."]})]}),e.jsx("div",{className:"schedule-frequency",role:"group","aria-label":"Schedule frequency",children:["once","daily","weekly"].map(h=>e.jsxs("button",{type:"button","aria-pressed":d===h,onClick:()=>o(h),children:[e.jsx("b",{children:h==="once"?"One time":h==="daily"?"Every day":"Weekly"}),e.jsx("span",{children:h==="once"?"A date range":h==="daily"?"Same time daily":"Choose days"})]},h))}),d==="once"?e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts",children:e.jsx("input",{type:"datetime-local",value:r.startAt?Qs(new Date(r.startAt)):"",onChange:h=>c(p=>({...p,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:r.endAt?Qs(new Date(r.endAt)):"",onChange:h=>c(p=>({...p,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:r.startTime??"",onChange:h=>c(p=>({...p,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:r.endTime??"",onChange:h=>c(p=>({...p,endTime:h.target.value}))})})]}),d==="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:()=>c(h=>({...h,weekdays:[1,2,3,4,5]})),children:"Weekdays"}),e.jsx("button",{type:"button",onClick:()=>c(h=>({...h,weekdays:[6,0]})),children:"Weekend"}),e.jsx("button",{type:"button",onClick:()=>c(h=>({...h,weekdays:Cs.map(p=>p.value)})),children:"Every day"})]})]}),e.jsx("div",{className:"schedule-day-grid",children:Cs.map(h=>{const p=(r.weekdays??[]).includes(h.value);return e.jsx("button",{type:"button","aria-pressed":p,title:h.label,onClick:()=>c(w=>({...w,weekdays:p?(w.weekdays??[]).filter(g=>g!==h.value):[...w.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:De.map(h=>e.jsx(Y,{label:h.label,checked:x.includes(h.id),disabled:!j(h.id),onChange:p=>c(w=>{const g=w.placements??["home"],m=p?[...g,h.id]:g.filter(R=>R!==h.id);return{...w,placements:m.length?[...new Set(m)]:g}})},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:r.priority,onChange:h=>c(p=>({...p,priority:Number(h.target.value)}))})})]}),e.jsx(Y,{label:"Schedule enabled",hint:"Pause it without losing its days and times.",checked:r.enabled,onChange:h=>c(p=>({...p,enabled:h}))}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx($,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx($,{variant:"primary",disabled:!b,onClick:()=>a(r),children:i?"Add rule":"Save rule"})]})]})})}const sn=["title","ratings","facts","summary"];function da(){var b;const s=Q("/admin/api/metadata-hero"),{busy:n,run:t}=ie(),{wrap:i}=le(),[a,l]=f.useState(null),[r,c]=f.useState(null);f.useEffect(()=>{a===null&&s.data&&l(s.data.settings.contentOrder),r===null&&s.data&&c(s.data.settings.timeRemainingColour??"green")},[a,s.data,r]);const d=a??sn,o=r??"green",x=((b=s.data)==null?void 0:b.options)??[],j=[...d,...x.map(h=>h.value).filter(h=>!d.includes(h))],u=(h,p)=>{const w=d.indexOf(h),g=w+p;if(w<0||g<0||g>=d.length)return;const m=[...d],R=m[w];m[w]=m[g],m[g]=R,l(m)},v=()=>t("save",async()=>{const h=await i(()=>q.post("/admin/api/metadata-hero",{contentOrder:d,timeRemainingColour:o}),"Metadata hero saved for every television.");h&&(s.set(h),l(h.settings.contentOrder),c(h.settings.timeRemainingColour))});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Metadata hero",intro:"Choose which content appears beside focused browse cards, and the order every television uses."}),e.jsx(U,{message:s.error}),s.loading?e.jsx(H,{rows:2}):e.jsxs("div",{className:"metadata-hero-layout",children:[e.jsxs(T,{title:"Content order",intro:"Enabled blocks are drawn from top to bottom. Changes apply to the whole household.",icon:"sliders",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:n==="save",onClick:()=>void v(),children:"Save metadata hero"}),e.jsx($,{onClick:()=>{l(sn),c("green")},children:"Restore defaults"})]}),children:[e.jsx("div",{className:"metadata-hero-order",children:j.map(h=>{const p=x.find(m=>m.value===h);if(!p)return null;const w=d.indexOf(h),g=w>=0;return e.jsxs("div",{className:"metadata-hero-order-row","data-enabled":g||void 0,children:[e.jsx("span",{className:"metadata-hero-order-number",children:g?w+1:"—"}),e.jsx(Y,{label:p.label,hint:p.description,checked:g,disabled:g&&d.length===1,onChange:m=>l(m?[...d,h]:d.filter(R=>R!==h))}),e.jsxs("div",{className:"metadata-hero-order-actions",children:[e.jsx($,{size:"sm",variant:"quiet",disabled:!g||w===0,title:`Move ${p.label} up`,onClick:()=>u(h,-1),children:"↑"}),e.jsx($,{size:"sm",variant:"quiet",disabled:!g||w===d.length-1,title:`Move ${p.label} down`,onClick:()=>u(h,1),children:"↓"})]})]},h)})}),e.jsxs("div",{className:"metadata-hero-colour",children:[e.jsx("b",{children:"Time-remaining text colour"}),e.jsx("p",{children:"Choose the caption colour used beside the resume progress bar."}),e.jsx(Be,{value:o,options:[{value:"green",label:"Emby green"},{value:"white",label:"White"}],onChange:c})]})]}),e.jsx(T,{title:"Television preview",intro:"A representative focused film at launcher scale and in the current order.",icon:"tv",children:e.jsxs("div",{className:"metadata-hero-preview","aria-label":"Metadata hero preview",children:[e.jsx("div",{className:"metadata-hero-preview-copy",children:d.map(h=>h==="title"?e.jsx("div",{className:"metadata-preview-title",children:"THE SOUTHERN LIGHT"},h):h==="ratings"?e.jsxs("div",{className:"metadata-preview-ratings",children:[e.jsx("b",{children:"IMDb 8.2"}),e.jsx("b",{children:"TMDb 81%"})]},h):h==="facts"?e.jsxs("div",{className:"metadata-preview-facts",children:[e.jsx("span",{children:"2026   •   1h 54m   •   M   •   Drama, Mystery"}),e.jsx("b",{children:"4K"}),e.jsx("b",{children:"5.1"})]},h):h==="recommendation_reason"?e.jsx("div",{className:"metadata-preview-reason",children:"Because you watched Harbour Lights"},h):h==="time_remaining"?e.jsxs("div",{className:"metadata-preview-progress","data-colour":o,children:[e.jsx("span",{children:e.jsx("i",{})}),e.jsx("b",{children:"47 minutes remaining"})]},h):h==="summary"?e.jsx("p",{className:"metadata-preview-summary",children:"A quiet coastal town follows an unexpected signal across the winter sky."},h):null)}),e.jsx("div",{className:"metadata-hero-preview-art","aria-hidden":"true"})]})})]})]})}function ha(){const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(null),o=s==null?void 0:s.features,x=(o==null?void 0:o.features)??[],j=(o==null?void 0:o.configuration)??[],u=(s==null?void 0:s.clients)??[],v=(o==null?void 0:o.revision)??0,b=(m,R,k,E)=>r(R,async()=>{await a(()=>q.post("/admin/api/features",{action:m,expectedRevision:v,overrides:E??{}}),k),d(null),await i()}),h=u.filter(m=>(m.capabilities??[]).includes("server_features_v1")).length,p=!!(o!=null&&o.safeMode),w=(m,R)=>({...Object.fromEntries(x.filter(k=>k.source==="override").map(k=>[k.key,k.enabled])),[m]:R}),g=(m,R)=>({...Object.fromEntries(j.map(k=>[k.key,k.value])),[m]:R});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Client configuration",intro:"One server-owned control plane for what Memby renders: flags, values, page composition and contextual discovery."}),e.jsx(U,{message:n}),t||!o?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(T,{title:"Thin client control plane",intro:"The gateway decides which sections, heroes and discovery rows are delivered. The TV remains a fast renderer of known components and safely ignores anything newer.",icon:"tv",children:e.jsxs("div",{className:"chips",children:[e.jsx("a",{className:"chip",href:"/admin/hero",children:"Hero sources and pinned overrides"}),e.jsx("a",{className:"chip",href:"/admin/recommendations",children:"For You and recommendation pools"}),e.jsx("a",{className:"chip",href:"/admin/engagement",children:"Contextual row engagement signals"}),e.jsx("a",{className:"chip",href:"/admin/clients",children:"Client versions and capabilities"})]})}),e.jsx(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:p?void 0:"danger",busy:l==="safe",onClick:()=>p?void b("leave-safe-mode","safe","Safe mode ended."):d({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:p?"Leave safe mode":"Enable safe mode"}),children:e.jsx(Ls,{tiles:[{label:"features active",value:`${x.filter(m=>m.enabled).length} / ${x.length}`},{label:"explicit overrides",value:y(x.filter(m=>m.source==="override").length)},{label:"televisions reporting the control plane",value:`${h} / ${u.length}`},{label:"published revision",value:`r${y(v)}`}]})}),e.jsx(T,{title:"Central configuration",intro:"Typed values use the same revisioned control plane as feature flags. Global values are safe defaults; user, device and experimental scopes are available to the client contract as features grow.",icon:"sliders",children:e.jsx("div",{className:"stack",children:j.map(m=>{const R=m.value;return e.jsxs("div",{className:"row",children:[e.jsxs("div",{className:"grow",children:[e.jsx("strong",{children:m.name}),e.jsxs("div",{className:"hint",children:[m.description," · ",m.scopes.join(", ")]}),e.jsx(de,{children:m.key})]}),m.type==="boolean"?e.jsx(Y,{label:R?"On":"Off",checked:!!R,disabled:l===m.key,onChange:k=>void r(m.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:v,values:g(m.key,k),overrides:Object.fromEntries(x.filter(E=>E.source==="override").map(E=>[E.key,E.enabled]))}),"Configuration saved."),await i()})}):m.type==="enum"?e.jsx("select",{value:String(R),disabled:l===m.key,onChange:k=>void r(m.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:v,values:g(m.key,k.target.value),overrides:Object.fromEntries(x.filter(E=>E.source==="override").map(E=>[E.key,E.enabled]))}),"Configuration saved."),await i()}),children:(m.options??[]).map(k=>e.jsx("option",{value:k,children:k},k))}):m.type==="json"?e.jsx("textarea",{rows:4,value:JSON.stringify(R,null,2),disabled:l===m.key,"aria-label":m.name,onChange:k=>{try{const E=JSON.parse(k.target.value);r(m.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:v,values:g(m.key,E),overrides:Object.fromEntries(x.filter(L=>L.source==="override").map(L=>[L.key,L.enabled]))}),"Configuration saved."),await i()})}catch{}}}):e.jsx("input",{type:"number",value:Number(R),min:m.min,max:m.max,disabled:l===m.key,onChange:k=>void r(m.key,async()=>{await a(()=>q.post("/admin/api/features",{action:"save",expectedRevision:v,values:g(m.key,Number(k.target.value)),overrides:Object.fromEntries(x.filter(E=>E.source==="override").map(E=>[E.key,E.enabled]))}),"Configuration saved."),await i()})})]},m.key)})})}),e.jsx(pe,{cols:"2",children:x.length===0?e.jsx(T,{title:"Nothing registered",icon:"sliders",children:e.jsx(ae,{children:"No server features are registered."})}):x.map(m=>e.jsxs(T,{title:m.name,intro:m.description,actions:e.jsx(S,{tone:m.enabled?"ok":void 0,children:m.enabled?"active":"off"}),footer:e.jsxs("span",{className:"hint",children:["↳ ",m.recovery]}),children:[e.jsx(Y,{label:m.enabled?"On":"Off",hint:"Changing this applies the feature policy to every compatible television.",checked:m.enabled,disabled:l===m.key,onChange:R=>d({action:"feature",key:m.key,enabled:R,title:`${R?"Turn on":"Turn off"} ${m.name}?`,body:`${R?"Enable":"Disable"} this feature for every compatible television. ${m.recovery}`,label:R?"Turn on":"Turn off"})}),e.jsxs("div",{className:"chips",children:[e.jsx(de,{children:m.key}),e.jsxs(de,{children:["protocol ",y(m.minimumProtocol),"+"]}),e.jsx(de,{tone:m.compatible?"ok":"warn",children:m.compatible?"server compatible":"compatibility blocked"}),e.jsx(de,{tone:"note",children:m.area})]})]},m.key))}),e.jsx(T,{children:e.jsxs("div",{className:"row",children:[e.jsx($,{disabled:!o.canRollback,onClick:()=>d({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:()=>d({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"}),p?e.jsx(S,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(S,{tone:"ok",children:["live · revision r",y(v)]})]})})]}),c?e.jsx(Se,{title:c.title,body:c.body,confirmLabel:c.label,destructive:c.action!=="rollback",busy:l===c.action,onConfirm:()=>void b(c.action==="feature"?"save":c.action,c.action==="feature"?c.key??"feature":c.action,`${c.label} done.`,c.action==="feature"&&c.key?w(c.key,!!c.enabled):void 0),onCancel:()=>d(null)}):null]})}function ua(){const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a,show:l}=le(),{busy:r,run:c}=ie(),[d,o]=f.useState(!0),[x,j]=f.useState("6.5"),[u,v]=f.useState(!1);f.useEffect(()=>{var h,p;u||!s||(o(((h=s.playbackPolicy)==null?void 0:h.prerollEnabled)!==!1),j(String((((p=s.playbackPolicy)==null?void 0:p.prerollDurationMs)??6500)/1e3)))},[s,u]);const b=()=>c("save",async()=>{const h=Number(x);if(!Number.isFinite(h)||h<1||h>30){l("The preroll duration must be between 1 and 30 seconds.","bad");return}await a(()=>q.post("/admin/api/playback-policy",{prerollEnabled:d,prerollDurationMs:Math.round(h*1e3)}),"Playback policy saved."),v(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Playback",intro:"Presentation policy sent with every playback launch."}),e.jsx(U,{message:n}),t?e.jsx(H,{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:d?e.jsxs(S,{tone:"ok",children:["on · ",x,"s"]}):e.jsx(S,{children:"off"}),footer:e.jsx($,{variant:"primary",busy:r==="save",onClick:()=>void b(),children:"Save playback policy"}),children:[e.jsx(Y,{label:"Show the preroll before a title starts",checked:d,onChange:h=>{o(h),v(!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:x,onChange:h=>{j(h.target.value),v(!0)}})})})]})]})}function ma(){const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(null),[o,x]=f.useState(null),[j,u]=f.useState(!1),v=s==null?void 0:s.subtitles,b=v==null?void 0:v.stored;f.useEffect(()=>{c||!v||d({bazarr:v.bazarrEnabled,openSubtitles:v.openSubtitlesEnabled,key:"",clearKey:!1,username:v.openSubtitlesUsername??"",password:"",clearLogin:!1})},[v,c]);const h=m=>d(R=>R&&{...R,...m}),p=()=>r("save",async()=>{c&&(await a(()=>q.post("/admin/api/subtitle-settings",{bazarrEnabled:c.bazarr,openSubtitlesEnabled:c.openSubtitles,openSubtitlesApiKey:c.key.trim(),clearOpenSubtitlesApiKey:c.clearKey,openSubtitlesUsername:c.username.trim(),openSubtitlesPassword:c.password,clearOpenSubtitlesLogin:c.clearLogin}),"Subtitle settings saved."),d(null),await i())}),w=()=>r("test",async()=>{x(null);const m=await a(()=>q.post("/admin/api/subtitle-test"));x((m==null?void 0:m.results)??[])}),g=()=>r("clear",async()=>{await a(()=>q.post("/admin/api/subtitle-settings",{action:"clear-stored"}),"Stored subtitles deleted."),u(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from."}),e.jsx(U,{message:n}),t||!v||!c?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"offered on televisions",value:v.available?"yes":"no",small:!0,icon:"captions",tone:v.available?"ok":void 0},{label:"providers on",value:y((v.bazarrEnabled&&v.bazarrConfigured?1:0)+(v.openSubtitlesEnabled?1:0)),icon:"list",tone:"note"},{label:"subtitles held",value:y((b==null?void 0:b.count)??0),icon:"database",tone:"data"},{label:"last fetched",value:P(b==null?void 0:b.latest),small:!0,icon:"clock"}]}),e.jsxs(pe,{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:v.bazarrConfigured?c.bazarr?e.jsx(S,{tone:"ok",children:"on"}):e.jsx(S,{children:"off"}):e.jsx(S,{children:"not configured"}),footer:e.jsx("span",{className:"hint",children:v.bazarrConfigured?`Configured at ${v.bazarrUrl}`:"Set MEMBY_BAZARR_URL and MEMBY_BAZARR_API_KEY to use Bazarr."}),children:e.jsx(Y,{label:"Offer Bazarr in the player",hint:"Off leaves every subtitle it has already written in place.",checked:c.bazarr,disabled:!v.bazarrConfigured,onChange:m=>h({bazarr:m})})}),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:v.openSubtitlesEnabled?e.jsx(S,{tone:v.openSubtitlesAccount?"ok":"warn",children:v.openSubtitlesAccount?"on · signed in":"on · anonymous"}):e.jsx(S,{children:v.openSubtitlesKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(Y,{label:"Offer OpenSubtitles in the player",hint:"Needs an API key. It cannot be switched on without one.",checked:c.openSubtitles,onChange:m=>h({openSubtitles:m})}),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:c.key,placeholder:v.openSubtitlesKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:m=>h({key:m.target.value})})}),e.jsx(Y,{label:"Remove the saved key",checked:c.clearKey,onChange:m=>h({clearKey:m})}),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:c.username,placeholder:"Not signed in",onChange:m=>h({username:m.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:c.password,onChange:m=>h({password:m.target.value})})})]}),e.jsx(Y,{label:"Sign out and forget the account",checked:c.clearLogin,onChange:m=>h({clearLogin:m})})]})]}),e.jsxs(T,{children:[e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>void p(),children:"Save subtitle settings"}),e.jsx($,{busy:l==="test",icon:"pulse",onClick:()=>void w(),children:"Test the providers"}),e.jsx("span",{className:"hint",children:v.featureEnabled?"A change applies to the next title opened; no app release is required.":"Downloading subtitles is switched off on the Features page, so nothing here is offered."})]}),o===null?null:o.length===0?e.jsx(ae,{children:"No provider is switched on, so there was nothing to ask."}):e.jsx("div",{className:"list",children:o.map(m=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:m.provider}),e.jsx("p",{children:m.message})]}),e.jsx("div",{className:"list-actions",children:e.jsx(S,{tone:m.ok?"ok":"bad",children:m.ok?"reachable":"not reachable"})})]},m.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:b!=null&&b.count?e.jsxs(S,{tone:"data",children:[y(b.count)," files · ",me(b.bytes)]}):e.jsx(S,{children:"nothing held"}),footer:e.jsx($,{variant:"danger",disabled:!(b!=null&&b.count),onClick:()=>u(!0),children:"Delete every stored subtitle"}),children:e.jsx(e.Fragment,{})})]}),j?e.jsx(Se,{title:"Delete every stored subtitle?",body:"Each one can be fetched again, at the cost of the download allowance that fetched it. Subtitles Bazarr wrote beside the media are untouched — those belong to Emby.",confirmLabel:"Delete",destructive:!0,busy:l==="clear",onConfirm:()=>void g(),onCancel:()=>u(!1)}):null]})}function pa(){const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(null),[o,x]=f.useState(!1),j=s==null?void 0:s.updatePolicy;f.useEffect(()=>{c||!j||d({version:j.latestVersion??"",url:j.downloadUrl??"",notes:j.notes??"",retireBelow:j.retireBelowVersion??"",required:!!j.minimumVersion&&j.minimumVersion===j.latestVersion,destructive:!!j.retireBelowVersion&&j.retireBelowVersion===j.latestVersion})},[j,c]);const u=p=>r(p?"save":"off",async()=>{c&&(await a(()=>q.post("/admin/api/update-policy",{enabled:p,latestVersion:c.version.trim(),downloadUrl:c.url.trim(),notes:c.notes.trim(),required:c.required,destructive:c.destructive,retireBelowVersion:c.retireBelow.trim()}),p?"Update policy saved.":"Update prompts turned off."),x(!1),d(null),await i())}),v=!!(j!=null&&j.minimumVersion)&&(j==null?void 0:j.minimumVersion)===(j==null?void 0:j.latestVersion),b=!!(j!=null&&j.retireBelowVersion)&&(j==null?void 0:j.retireBelowVersion)===(j==null?void 0:j.latestVersion),h=p=>d(w=>w&&{...w,...p});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"App updates",intro:"Publish an optional or a required client update."}),e.jsx(U,{message:n}),t||!c?e.jsx(H,{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:j!=null&&j.enabled?e.jsxs(S,{tone:v?"warn":"ok",children:[b?"sign-out · ":v?"required · ":"optional · ",j.latestVersion]}):e.jsx(S,{children:"off"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>c.required?x(!0):void u(!0),children:"Save policy"}),e.jsx($,{busy:l==="off",onClick:()=>void u(!1),children:"Turn prompts off"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Latest version",children:e.jsx("input",{type:"text",value:c.version,placeholder:"0.2.63",onChange:p=>h({version:p.target.value})})}),e.jsx(D,{label:"APK URL",children:e.jsx("input",{type:"text",value:c.url,placeholder:"https://nas/memby/memby-0.2.63.apk",onChange:p=>h({url:p.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:c.notes,placeholder:"One line the viewer reads",onChange:p=>h({notes:p.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:c.retireBelow,placeholder:"0.2.44",onChange:p=>h({retireBelow:p.target.value})})}),e.jsx(Y,{label:"Require this update",hint:"Blocks the home screen on every television below this version.",checked:c.required,onChange:p=>h({required:p})}),e.jsx(Y,{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:c.destructive,onChange:p=>h(p?{destructive:!0,required:!0,retireBelow:c.version.trim()}:{destructive:!1,retireBelow:c.retireBelow.trim()===c.version.trim()?"":c.retireBelow})})]}),o&&c?e.jsx(Se,{title:c.destructive?"Sign every older television out?":"Require this update?",body:c.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:c.destructive,busy:l==="save",onConfirm:()=>void u(!0),onCancel:()=>x(!1)}):null]})}const xa=[{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 ja(s){const n=[...xa];for(const t of[s.defaultIntervalSeconds,s.intervalSeconds])t>0&&!n.some(i=>i.value===t)&&n.push({value:t,label:Ce(t).replace(/^every /,"Every ")});return n.sort((t,i)=>t.value-i.value)}const Qe=s=>s.defaultIntervalSeconds>0&&s.intervalSeconds!==s.defaultIntervalSeconds;function js(s){return s==="failed"?"bad":s==="running"?"info":s==="skipped"?"warn":"ok"}function ds(s){if(s.running)return{state:"running",label:"Running",tone:"info",title:"Running right now"};if(!s.enabled)return{state:"disabled",label:"Disabled",tone:"warn",title:"Switched off — it will not run on its schedule"};const n=s.lastRun;return n?n.status==="failed"?{state:"failed",label:"Failed",tone:"bad",title:n.error||"The last run failed"}:n.status==="skipped"?{state:"skipped",label:"Skipped",tone:"warn",title:n.detail||"The last run declined to do anything"}:{state:"successful",label:"Successful",tone:"ok",title:"The last run finished cleanly"}:{state:"never",label:"Never run",tone:void 0,title:"This task has not run since the gateway started recording"}}function ga(s){if(s.state==="running"||s.state==="successful")return"ok";if(s.state==="failed")return"bad";if(s.state==="disabled"||s.state==="skipped")return"warn"}const nn={failed:0,running:1,skipped:2,disabled:3,never:4,successful:5};function va(s,n){const t=nn[ds(s).state]-nn[ds(n).state];if(t!==0)return t;if(!!s.group!=!!n.group)return s.group?-1:1;const i=s.group.localeCompare(n.group);return i!==0?i:s.name.localeCompare(n.name)}const fa=2e4,ba=3e3;function ya(){const{wrap:s}=le(),{busy:n,run:t}=ie(),[i,a]=f.useState(!1),{data:l,error:r,loading:c,reload:d}=Q("/admin/api/tasks?limit=60",{pollMs:i?ba:fa}),o=(l==null?void 0:l.tasks)??[],x=o.some(p=>p.running);x!==i&&a(x);const j=p=>t(p.id,async()=>{await s(()=>q.post(`/admin/api/tasks/${encodeURIComponent(p.id)}/run`),`${p.name} started.`),await d()}),u=o.filter(p=>{var w;return((w=p.lastRun)==null?void 0:w.status)==="failed"}).length,v=o.filter(Qe).length,b=o.filter(p=>!p.enabled).length,h=[...o].sort(va);return e.jsxs(e.Fragment,{children:[e.jsx(B,{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:r}),c?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"Tasks",value:y(o.length),icon:"clock",tone:"info"},{label:"Running now",value:y(o.filter(p=>p.running).length),icon:"pulse",tone:x?"ok":void 0},{label:"Last run failed",value:y(u),icon:"alert",tone:u>0?"bad":void 0},{label:"Switched off",value:y(b),icon:"power",tone:b>0?"warn":void 0},{label:"Retimed",value:y(v),icon:"clock",tone:v>0?"note":void 0}]}),u>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,e.jsx(T,{title:"Tasks",intro:"Anything wrong sorts to the top. Open a task to change its schedule, switch it off, or read its own run history.",icon:"clock",tone:"info",children:e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Service"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Status"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:h.length===0?e.jsx(X,{columns:8,children:"This gateway has no scheduled tasks registered."}):h.map(p=>{const w=ds(p),g=p.lastRun,m=`/admin/tasks/${encodeURIComponent(p.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(J,{className:"table-row-link",to:m,children:p.name}),e.jsx("span",{className:"table-sub",children:p.description})]}),e.jsx("td",{className:"muted nowrap",children:p.group||"Other"}),e.jsxs("td",{className:"nowrap",children:[Ce(p.intervalSeconds),Qe(p)?e.jsxs("span",{className:"table-sub",children:["default ",Ce(p.defaultIntervalSeconds)]}):null]}),e.jsxs("td",{className:"nowrap muted",title:g?P(g.startedAt):void 0,children:[g?ge(g.startedAt):"never",g!=null&&g.error?e.jsx("span",{className:"table-sub",children:g.error}):null]}),e.jsx("td",{className:"num muted",children:g?we(g.durationMs):"—"}),e.jsx("td",{className:"nowrap muted",children:p.enabled?p.running?"now":ms(p.nextRun):"—"}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":ga(w)}),e.jsx(S,{tone:w.tone,children:e.jsx("span",{title:w.title,children:w.label})})]})}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx($,{size:"sm",icon:"play",busy:n===p.id,disabled:p.running,title:`Run ${p.name} now`,onClick:()=>void j(p)}),e.jsx(J,{className:"table-row-link",to:m,children:"Details"})]})})]},p.id)})})]})})}),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(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:((l==null?void 0:l.runs.length)??0)===0?e.jsx(X,{columns:6,children:"No task has run yet."}):l==null?void 0:l.runs.map(p=>{var w;return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(p.startedAt),children:ge(p.startedAt)}),e.jsx("td",{children:e.jsx(J,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(p.taskId)}`,children:((w=o.find(g=>g.id===p.taskId))==null?void 0:w.name)??p.taskId})}),e.jsx("td",{className:"muted",children:p.trigger}),e.jsx("td",{children:e.jsx(S,{tone:js(p.status),children:p.status})}),e.jsx("td",{className:"num muted",children:we(p.durationMs)}),e.jsx("td",{className:"muted",children:p.error||p.detail||"—"})]},p.id)})})]})})})]})]})}const wa=2e4,ka=3e3,Na=100;function Sa(s){const n=s.filter(l=>l.status!=="running"),t=n.filter(l=>l.status==="failed").length,i=n.filter(l=>l.durationMs>0),a=i.length?i.reduce((l,r)=>l+r.durationMs,0)/i.length:0;return{runs:n.length,failed:t,averageMs:a,timed:i.length}}function Ca(){var w;const{taskId:s=""}=He(),{wrap:n}=le(),{busy:t,run:i}=ie(),[a,l]=f.useState(!1),{data:r,error:c,loading:d,reload:o}=Q(`/admin/api/tasks?task=${encodeURIComponent(s)}&limit=${Na}`,{pollMs:a?ka:wa,enabled:!!s}),x=r==null?void 0:r.tasks.find(g=>g.id===s),j=!!(x!=null&&x.running);j!==a&&l(j);const u=(r==null?void 0:r.runs)??[],v=x?ds(x):void 0,b=Sa(u),h=(g,m,R)=>i(g,async()=>{await n(()=>q.put(`/admin/api/tasks/${encodeURIComponent(s)}`,R),m),await o()}),p=g=>i("run",async()=>{await n(()=>q.post(`/admin/api/tasks/${encodeURIComponent(s)}/run`),`${g.name} started.`),await o()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:(x==null?void 0:x.name)||s,intro:x==null?void 0:x.description,icon:"clock",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(J,{to:"/admin/tasks",children:"Scheduled tasks"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:(x==null?void 0:x.name)||s})]}),actions:x?e.jsx($,{variant:"primary",icon:"play",busy:t==="run",disabled:x.running,onClick:()=>void p(x),children:x.running?"Running":"Run now"}):void 0}),e.jsx(U,{message:c}),d?e.jsx(H,{}):x?e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"Status",value:(v==null?void 0:v.label)??"—",small:!0,icon:"pulse",tone:v==null?void 0:v.tone},{label:"Service",value:x.group||"Other",small:!0,icon:"chip",tone:"info"},{label:"Runs every",value:Ce(x.intervalSeconds),small:!0,icon:"clock",tone:Qe(x)?"note":void 0},{label:"Next run",value:x.enabled?x.running?"now":ms(x.nextRun):"not scheduled",small:!0,icon:"history",tone:x.enabled?void 0:"warn"},{label:`Failed of the last ${y(b.runs)}`,value:y(b.failed),icon:"alert",tone:b.failed>0?"bad":void 0},{label:`Average of ${y(b.timed)} timed runs`,value:b.timed?we(Math.round(b.averageMs)):"—",small:!0,icon:"chart",tone:"data"}]}),(w=x.lastRun)!=null&&w.error?e.jsxs(je,{tone:"bad",children:["The last run failed: ",x.lastRun.error]}):null,e.jsxs(T,{title:"Schedule",intro:"How often the gateway runs this on its own, and whether it runs it at all.",icon:"sliders",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx(D,{label:"How often it runs",children:e.jsx("select",{"aria-label":`How often ${x.name} runs`,value:x.intervalSeconds,disabled:t==="interval"||x.running,onChange:g=>void h("interval",`${x.name} now runs ${Ce(Number(g.target.value))}.`,{intervalSeconds:Number(g.target.value)}),children:ja(x).map(g=>e.jsxs("option",{value:g.value,children:[g.label,g.value===x.defaultIntervalSeconds?" (default)":""]},g.value))})}),Qe(x)?e.jsxs($,{icon:"refresh",busy:t==="interval",disabled:x.running,onClick:()=>void h("interval",`${x.name} back to its default cadence.`,{intervalSeconds:0}),children:["Back to ",Ce(x.defaultIntervalSeconds)]}):null]}),e.jsx(Y,{label:"Run this on its schedule",hint:"Switched off, the gateway leaves it alone. You can still start it by hand.",checked:x.enabled,disabled:t==="enabled",onChange:g=>void h("enabled",g?`${x.name} switched on.`:`${x.name} switched off.`,{enabled:g})}),Qe(x)?e.jsxs(je,{tone:"note",children:["This task is retimed: its code asks for ",Ce(x.defaultIntervalSeconds)," and it is set to ",Ce(x.intervalSeconds),"."]}):null]}),e.jsx(T,{title:"Run history",intro:"This task alone, newest first — which is what separates a job that fails occasionally from one that has stopped working.",icon:"history",tone:"note",children:e.jsx(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:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Detail"})]})}),e.jsx("tbody",{children:u.length===0?e.jsx(X,{columns:5,children:"This task has not run yet."}):u.map(g=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(g.startedAt),children:ge(g.startedAt)}),e.jsx("td",{className:"muted",children:g.trigger}),e.jsx("td",{children:e.jsx(S,{tone:js(g.status),children:g.status})}),e.jsx("td",{className:"num muted",children:we(g.durationMs)}),e.jsx("td",{className:"muted",children:g.error||g.detail||"—"})]},g.id))})]})})})]}):e.jsx(T,{title:"No such task",icon:"alert",tone:"bad",children:e.jsxs(ae,{children:["This gateway has no task called ",e.jsx("code",{children:s}),". It may have been renamed or removed."," ",e.jsx(J,{to:"/admin/tasks",children:"Back to scheduled tasks"}),"."]})})]})}function Ue(s){switch(s){case"error":return"bad";case"healthy":return"ok";case"running":return"info";case"disabled":return"warn";default:return}}function Un(s){return Ue(s)}const tn={error:0,running:1,idle:2,healthy:3,disabled:4,unconfigured:5};function Ma(s,n){const t=tn[s.status]-tn[n.status];return t!==0?t:s.name.localeCompare(n.name)}const Ta=s=>!!(s&&(s.processed||s.changed||s.skipped||s.failed));function es(s){if(!Ta(s)||!s)return"";const n=[];return s.processed&&n.push(`${s.processed.toLocaleString()} checked`),s.changed&&n.push(`${s.changed.toLocaleString()} changed`),s.skipped&&n.push(`${s.skipped.toLocaleString()} skipped`),s.failed&&n.push(`${s.failed.toLocaleString()} failed`),n.join(" · ")}function Ds(s){if(s.status==="running")return"Running…";if(s.error)return s.error;if(s.detail)return s.detail;const n=es(s.counts);return n||"No changes"}function $a(s){return s.powers.length?`Switching ${s.name} off also stops: ${s.powers.join("; ")}.`:""}const Ra=2e4,Ea=3e3;function Aa(){const[s,n]=f.useState(!1),{data:t,error:i,loading:a}=Q("/admin/api/integrations/services?limit=40",{pollMs:s?Ea:Ra}),l=((t==null?void 0:t.services)??[]).some(u=>u.running);l!==s&&n(l);const r=[...(t==null?void 0:t.services)??[]].sort(Ma),c=(t==null?void 0:t.runs)??[],d=r.filter(u=>u.status==="error"),o=r.filter(u=>u.running),x=r.filter(u=>u.configured&&!u.enabled),j=r.filter(u=>!u.configured);return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Integrations",intro:"The services Memby depends on: whether each one is configured, whether it is working, what it is doing right now and what it last managed to do."}),e.jsx(U,{message:i}),a?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"Integrations",value:y(r.length),icon:"plug",tone:"info"},{label:"Not working",value:y(d.length),icon:"alert",tone:d.length>0?"bad":void 0},{label:"Running now",value:y(o.length),icon:"pulse",tone:o.length>0?"ok":void 0},{label:"Switched off",value:y(x.length),icon:"power",tone:x.length>0?"warn":void 0},{label:"Not configured",value:y(j.length),icon:"sliders",tone:j.length>0?"note":void 0}]}),d.length>0?e.jsxs(je,{tone:"bad",children:[d.map(u=>u.name).join(", ")," ",d.length===1?"is not answering":"are not answering",". A failed run also publishes an administrative event, so it is in the activity feed and wherever your webhooks send it — you did not have to be looking at this page."]}):null,e.jsx(T,{title:"Services",intro:"Anything wrong sorts to the top. Open a service to switch it off, read its settings, or see everything it has done.",icon:"plug",tone:"info",children:e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Integration"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Enabled"})]})}),e.jsx("tbody",{children:r.length===0?e.jsx(X,{columns:6,children:"This gateway has no integrations."}):r.map(u=>e.jsx(Ia,{service:u},u.id))})]})})}),e.jsx(T,{title:"Recent activity",intro:"Every integration's work together and in order, which is what shows two of them getting in each other's way. This is what Memby attempted and what came of it — the Logs page is where the technical detail behind a failure lives.",icon:"history",tone:"note",children:e.jsx(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:"Integration"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:c.length===0?e.jsx(X,{columns:6,children:"No integration has run yet."}):c.map(u=>{const v=r.find(h=>h.id===u.integrationId),b=v==null?void 0:v.tasks.find(h=>h.id===u.taskId);return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(u.startedAt),children:ge(u.startedAt)}),e.jsx("td",{className:"nowrap",children:e.jsx(J,{className:"table-row-link",to:`/admin/integrations/${encodeURIComponent(u.integrationId??"")}`,children:(v==null?void 0:v.name)??u.integrationId})}),e.jsx("td",{className:"muted",children:(b==null?void 0:b.name)??u.taskId}),e.jsx("td",{children:e.jsx(S,{tone:js(u.status),children:u.status})}),e.jsxs("td",{className:"muted",children:[Ds(u),u.error&&es(u.counts)?e.jsx("span",{className:"table-sub",children:es(u.counts)}):null]}),e.jsx("td",{className:"num muted",children:we(u.durationMs)})]},u.id)})})]})})})]})]})}function Ia({service:s}){const n=`/admin/integrations/${encodeURIComponent(s.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(J,{className:"table-row-link",to:n,children:s.name}),e.jsx("span",{className:"table-sub",children:s.address||s.summary})]}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":Un(s.status)}),e.jsx(S,{tone:Ue(s.status),children:e.jsx("span",{title:s.detail||s.statusLabel,children:s.statusLabel})})]})}),e.jsx("td",{className:"nowrap muted",title:s.lastRun?P(s.lastRun.startedAt):void 0,children:s.lastRun?ge(s.lastRun.startedAt):"never"}),e.jsx("td",{className:"muted",children:s.lastRun?Ds(s.lastRun):s.detail||"—"}),e.jsx("td",{className:"nowrap muted",children:s.configured?s.enabled?s.running?"now":ms(s.nextRun):"off":"—"}),e.jsx("td",{className:"nowrap",children:s.configured?s.enabled?e.jsx(S,{tone:"ok",children:"On"}):e.jsx(S,{tone:"warn",children:"Off"}):e.jsx(S,{children:"not configured"})})]})}const La={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 Da(){const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(!1),[o,x]=f.useState(""),[j,u]=f.useState(!1),[v,b]=f.useState([]),[h,p]=f.useState(!1),w=s==null?void 0:s.mdblist;f.useEffect(()=>{h||!w||(d(w.enabled),b(w.sources??[]))},[w,h]);const g=()=>r("save",async()=>{await a(()=>q.post("/admin/api/mdblist-settings",{enabled:c,apiKey:o.trim(),clearApiKey:j,sources:v}),"Ratings settings saved."),x(""),u(!1),p(!1),await i()}),m=(w==null?void 0:w.cachedTitles)??0;return e.jsxs(e.Fragment,{children:[e.jsx(U,{message:n}),t||!w?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsxs(pe,{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:c?e.jsxs(S,{tone:"ok",children:["on · ",v.length," sources"]}):e.jsx(S,{children:w.apiKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(Y,{label:"Show external ratings on televisions",hint:"Off leaves the stored ratings in place.",checked:c,onChange:R=>{d(R),p(!0)}}),e.jsx(D,{label:"API key",hint:"Leave blank to keep the key that is already saved.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:o,placeholder:w.apiKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:R=>x(R.target.value)})}),e.jsx(Y,{label:"Remove the saved key",checked:j,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:(w.availableSources??[]).length===0?e.jsx(ae,{children:"No rating sources are available."}):e.jsx("div",{className:"checks columns",children:(w.availableSources??[]).map(R=>e.jsx(Y,{label:La[R]??R,checked:v.includes(R),onChange:k=>{p(!0),b(E=>k?[...E,R]:E.filter(L=>L!==R))}},R))})})]}),e.jsx(T,{children:e.jsxs("div",{className:"row",children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>void g(),children:"Save ratings settings"}),e.jsx("span",{className:"hint",children:m?"Ratings are fetched as televisions browse, never on the request path.":"No ratings stored yet. They are saved as televisions browse the library."})]})})]})]})}const qa={id:"",name:"Discord",url:"",enabled:!0,events:[]};function Fa(){const{wrap:s,show:n}=le(),{busy:t,run:i}=ie(),{data:a,error:l,loading:r,reload:c}=Q("/admin/api/integrations",{pollMs:6e4}),[d,o]=f.useState(null),[x,j]=f.useState(null),u=(a==null?void 0:a.catalogue)??[],v=(a==null?void 0:a.integrations)??[],b=g=>o({id:g.id,name:g.name,url:"",enabled:g.enabled,events:g.events??[]}),h=()=>i("save",async()=>{if(!d)return;await s(()=>q.post("/admin/api/integrations",d),d.id?"Integration saved.":"Integration added.")&&(o(null),await c())}),p=g=>i("remove",async()=>{await s(()=>q.del(`/admin/api/integrations/${encodeURIComponent(g.id)}`),`${g.name} removed.`),j(null),await c()}),w=g=>i(`test:${g.id}`,async()=>{const m=await s(()=>q.post(`/admin/api/integrations/${encodeURIComponent(g.id)}/test`));m&&n(m.message,m.ok?"ok":"bad"),await c()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Event webhooks",intro:"Send administrative events to somewhere you already look. Events pass through the gateway's own event layer, so nothing about authentication or scheduled tasks knows Discord exists — and a second kind of destination is a change here rather than everywhere.",actions:e.jsx($,{variant:"primary",icon:"plus",onClick:()=>o(qa),children:"Add a webhook"})}),e.jsx(U,{message:l}),((a==null?void 0:a.dropped)??0)>0?e.jsxs(je,{tone:"warn",children:[y((a==null?void 0:a.dropped)??0)," events could not be queued for delivery. The queue is deliberately lossy — a slow endpoint must never hold up a television signing in — but a number growing here means a destination is not keeping up."]}):null,r?e.jsx(H,{}):v.length===0&&!d?e.jsx(T,{title:"Nothing configured",icon:"plug",tone:"note",children:e.jsx(ae,{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."})}):v.map(g=>e.jsx(_a,{integration:g,catalogue:u,busy:t,onEdit:()=>b(g),onTest:()=>void w(g),onRemove:()=>j(g)},g.id)),d?e.jsx(Ua,{draft:d,catalogue:u,busy:t==="save",onChange:o,onSave:()=>void h(),onCancel:()=>o(null)}):null,x?e.jsx(Se,{title:`Remove ${x.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 p(x),onCancel:()=>j(null)}):null]})}function Oa(){const{wrap:s}=le(),{busy:n,run:t}=ie(),{data:i,error:a,loading:l,reload:r}=Q("/admin/api/sonarr-request-policy"),[c,d]=f.useState(0),[o,x]=f.useState(!1);f.useEffect(()=>{i&&(d(i.qualityProfileId),x(i.searchImmediately))},[i]);const j=()=>t("sonarr-request-policy",async()=>{await s(()=>q.post("/admin/api/sonarr-request-policy",{qualityProfileId:c,searchImmediately:o}),"Sonarr TV request policy saved."),await r()}),u=i==null?void 0:i.profiles.find(v=>v.id===c);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(S,{tone:"ok",children:"configured"}):e.jsx(S,{tone:"warn",children:"needs attention"}),footer:e.jsx($,{variant:"primary",busy:n==="sonarr-request-policy",disabled:l||c<=0,onClick:()=>void j(),children:"Save Sonarr policy"}),children:[e.jsx(U,{message:a??(i==null?void 0:i.error)??""}),l?e.jsx(H,{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:c,onChange:v=>d(Number(v.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(v=>e.jsxs("option",{value:v.id,children:[v.name,v.recommended?" — recommended (720p)":""]},v.id))]})})}),e.jsx(Y,{label:"Search for episodes immediately after request",hint:"Off adds and monitors the series without searching its backlog. Enable only when requests should start an immediate episode search.",checked:o,onChange:x}),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 ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function Pa(){const{wrap:s}=le(),{busy:n,run:t}=ie(),{data:i,error:a,loading:l,reload:r}=Q("/admin/api/radarr-request-policy"),[c,d]=f.useState(0),[o,x]=f.useState(!1);f.useEffect(()=>{i&&(d(i.qualityProfileId),x(i.searchImmediately))},[i]);const j=()=>t("radarr-request-policy",async()=>{await s(()=>q.post("/admin/api/radarr-request-policy",{qualityProfileId:c,searchImmediately:o}),"Radarr movie request policy saved."),await r()}),u=i==null?void 0:i.profiles.find(v=>v.id===c);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(S,{tone:"ok",children:"configured"}):e.jsx(S,{tone:"warn",children:"needs attention"}),footer:e.jsx($,{variant:"primary",busy:n==="radarr-request-policy",disabled:l||c<=0,onClick:()=>void j(),children:"Save Radarr policy"}),children:[e.jsx(U,{message:a??(i==null?void 0:i.error)??""}),l?e.jsx(H,{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:c,onChange:v=>d(Number(v.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(v=>e.jsxs("option",{value:v.id,children:[v.name,v.recommended?" — recommended (720p)":""]},v.id))]})})}),e.jsx(Y,{label:"Search for the film immediately after request",hint:"Off adds and monitors the film without asking Radarr to search. Enable only when requests should start an immediate search.",checked:o,onChange:x}),u?e.jsxs(je,{tone:"info",children:["Requested films will use ",e.jsx("b",{children:u.name})," (profile ID ",u.id,"), remain monitored, and ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function _a({integration:s,catalogue:n,busy:t,onEdit:i,onTest:a,onRemove:l}){const r=s.health,c=!r.lastFailure||r.lastSuccess&&r.lastSuccess>r.lastFailure,d=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(S,{tone:"ok",children:"on"}):e.jsx(S,{tone:"warn",children:"off"}),r.deliveries>0?e.jsx(S,{tone:c?"ok":"bad",children:c?"delivering":"failing"}):e.jsx(S,{children:"never used"}),e.jsx($,{size:"sm",icon:"pulse",busy:t===`test:${s.id}`,onClick:a,children:"Test"}),e.jsx($,{size:"sm",onClick:i,children:"Edit"}),e.jsx($,{size:"sm",variant:"danger",icon:"trash",onClick:l,title:"Remove"})]}),children:[e.jsxs("div",{className:"list",children:[e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Events sent"}),e.jsx("p",{children:d.length===0?"None selected — this destination is configured but will never post anything.":d.map(o=>{var x;return((x=n.find(j=>j.type===o))==null?void 0:x.label)??o}).join(", ")})]})}),e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last delivered"}),e.jsx("p",{children:r.lastSuccess?P(r.lastSuccess):"never"})]}),e.jsx("div",{className:"list-actions",children:r.deliveries>0?e.jsxs("span",{className:"quiet",children:[y(r.deliveries)," attempts, ",y(r.failures)," failed"]}):null})]}),r.lastFailure?e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last failure"}),e.jsxs("p",{children:[P(r.lastFailure),r.lastError?` — ${r.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(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(o.attemptedAt),children:ge(o.attemptedAt)}),e.jsx("td",{className:"muted",children:o.eventType}),e.jsx("td",{children:o.success?e.jsx(S,{tone:"ok",children:o.statusCode||"ok"}):e.jsx(S,{tone:"bad",children:o.error||o.statusCode||"failed"})}),e.jsx("td",{className:"num muted",children:we(o.durationMs)})]},o.id))})]})}):e.jsx(G,{children:e.jsx("table",{children:e.jsx("tbody",{children:e.jsx(X,{columns:4,children:"Nothing has been delivered through this webhook yet."})})})})]})}function Ua({draft:s,catalogue:n,busy:t,onChange:i,onSave:a,onCancel:l}){const r=[...new Set(n.map(d=>d.group))],c=(d,o)=>i({...s,events:o?[...s.events,d]:s.events.filter(x=>x!==d)});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:a,children:s.id?"Save":"Add"}),e.jsx($,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx("span",{className:"spacer"}),s.events.length===0?e.jsx("span",{className:"quiet",children:"Nothing selected — this destination would never post."}):e.jsxs("span",{className:"quiet",children:[s.events.length," events selected"]})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Name",hint:"What this destination is called in the console.",children:e.jsx("input",{type:"text",value:s.name,onChange:d=>i({...s,name:d.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:d=>i({...s,url:d.target.value})})})]}),e.jsx(Y,{label:"Enabled",hint:"Off keeps the configuration and stops the posts.",checked:s.enabled,onChange:d=>i({...s,enabled:d})}),r.map(d=>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:d})})}),n.filter(o=>o.group===d).map(o=>e.jsx(Y,{label:o.label,hint:o.description,checked:s.events.includes(o.type),onChange:x=>c(o.type,x)},o.type))]},d))]})}const Wa=2e4,Ba=3e3;function Va(){const{integrationId:s=""}=He(),{wrap:n,show:t}=le(),{busy:i,run:a}=ie(),[l,r]=f.useState(!1),{data:c,error:d,loading:o,reload:x}=Q(`/admin/api/integrations/services/${encodeURIComponent(s)}?limit=80`,{pollMs:l?Ba:Wa,enabled:!!s}),j=c==null?void 0:c.service,u=(c==null?void 0:c.runs)??[],v=!!(j!=null&&j.running);v!==l&&r(v);const b=g=>a("enabled",async()=>{await n(()=>q.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/enabled`,{enabled:g}),`${(j==null?void 0:j.name)??"Integration"} ${g?"enabled":"disabled"}.`),await x()}),h=()=>a("test",async()=>{const g=await n(()=>q.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/test`));g&&t(g.reachable?`${j==null?void 0:j.name} answered in ${we(g.latencyMs)}.`:g.error||`${j==null?void 0:j.name} did not answer.`,g.reachable?"ok":"bad"),await x()}),p=(g,m)=>a(g,async()=>{await n(()=>q.post(`/admin/api/tasks/${encodeURIComponent(g)}/run`),`${m} started.`),await x()});if(o||!j)return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Integration",intro:"One external service and everything it has done."}),e.jsx(U,{message:d}),o?e.jsx(H,{}):e.jsx(je,{tone:"warn",children:"No such integration."})]});const w=u.filter(g=>g.status==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:j.name,intro:j.summary,actions:e.jsx(J,{className:"table-row-link",to:"/admin/integrations",children:"All integrations"})}),e.jsx(U,{message:d}),e.jsx(ce,{tiles:[{label:"Status",value:j.statusLabel,small:!0,icon:"pulse",tone:Ue(j.status)},{label:"Runs recorded",value:y(j.runs),icon:"history",tone:"data"},{label:"Failed runs",value:y(j.failures),icon:"alert",tone:j.failures>0?"bad":void 0},{label:"Last worked",value:j.lastSuccessAt?ge(j.lastSuccessAt):"never",small:!0,icon:"check",tone:j.lastSuccessAt?"ok":void 0}]}),e.jsxs(T,{title:"Connection",intro:"Address and credentials come from this gateway's environment; the switch is stored on the server and applies to the whole household.",icon:"plug",tone:Ue(j.status)??"info",actions:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":Un(j.status)}),e.jsx(S,{tone:Ue(j.status),children:j.statusLabel})]}),footer:j.configured&&j.probed?e.jsx($,{icon:"sync",busy:i==="test",onClick:()=>void h(),children:"Test connection"}):void 0,children:[j.detail?e.jsx(je,{tone:Ue(j.status),children:j.detail}):null,j.configured?e.jsxs(e.Fragment,{children:[e.jsx(Y,{label:`${j.name} enabled`,hint:j.enabled?$a(j):`Memby is not calling ${j.name} or scheduling any of its work.`,checked:j.enabled,disabled:i==="enabled",onChange:g=>void b(g)}),e.jsx(Ve,{rows:[{label:"Address",value:j.address||"—"},...(j.facts??[]).map(g=>({label:g.label,value:g.tone?e.jsx(S,{tone:g.tone==="warn"?"warn":"ok",children:g.value}):g.value})),{label:"Last checked",value:j.probed?j.health?`${ge(j.health.checkedAt)} · ${we(j.health.latencyMs)}`:"not yet":"Not probed — see below"}]}),j.probed?null:e.jsxs(je,{children:[j.name," is not probed for reachability: its allowance is bought by the day, and spending a request of it to draw a status would compete with the televisions for the thing being reported on. Its health comes from the run history below."]})]}):e.jsxs(je,{tone:"warn",children:["This gateway has no address or credential for ",j.name,". Set them in the deployment's environment and restart the container; there is nothing to switch until then."]})]}),j.powers.length>0?e.jsx(T,{title:"What depends on this",intro:"Switching the service off stops all of it. Nothing below fails quietly — it stops being offered.",icon:"journey",tone:"note",children:e.jsx("div",{className:"list",children:j.powers.map(g=>e.jsxs("div",{className:"list-item",children:[e.jsx(ss,{name:"check",tone:"ok"}),e.jsx("span",{className:"list-body",children:g})]},g))})}):null,e.jsx(za,{id:j.id}),e.jsx(T,{title:"Scheduled work",intro:"The background jobs belonging to this service. Cadence and the per-job switch live on each job's own page; this is where you start one by hand.",icon:"clock",tone:"info",children:e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:j.tasks.length===0?e.jsx(X,{columns:5,children:"This service has no scheduled work: Memby calls it when a television asks for something rather than on a timer."}):j.tasks.map(g=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(J,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(g.id)}`,children:g.name}),e.jsx("span",{className:"table-sub",children:g.description})]}),e.jsx("td",{className:"nowrap muted",children:Ce(g.intervalSeconds)}),e.jsx("td",{className:"nowrap muted",title:g.lastRun?P(g.lastRun.startedAt):void 0,children:g.lastRun?ge(g.lastRun.startedAt):"never"}),e.jsx("td",{className:"nowrap muted",children:!g.enabled||!j.enabled?"off":g.running?"now":ms(g.nextRun)}),e.jsx("td",{className:"nowrap",children:e.jsx($,{size:"sm",icon:"play",busy:i===g.id,disabled:g.running,title:`Run ${g.name} now`,onClick:()=>void p(g.id,g.name)})})]},g.id))})]})})}),e.jsx(T,{title:"Run history",intro:"What Memby attempted on this service's behalf and what came of it. This is not the application log — a failure here gives the reason, and Logs is where the technical detail behind it lives.",icon:"history",tone:"note",actions:w>0?e.jsx(J,{className:"table-row-link",to:`/admin/logs?q=${encodeURIComponent(j.id)}`,children:"Open the logs"}):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:"Started"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Checked"}),e.jsx("th",{className:"num",children:"Changed"}),e.jsx("th",{className:"num",children:"Skipped"}),e.jsx("th",{className:"num",children:"Failed"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:u.length===0?e.jsxs(X,{columns:10,children:["Nothing has run for ",j.name," yet."]}):u.map(g=>e.jsx(Ha,{run:g,service:j},g.id))})]})})})]})}function Ha({run:s,service:n}){var a,l,r,c;const t=n.tasks.find(d=>d.id===s.taskId),i=d=>d?y(d):"—";return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(s.startedAt),children:ge(s.startedAt)}),e.jsx("td",{className:"muted",children:(t==null?void 0:t.name)??s.taskId}),e.jsx("td",{className:"muted",children:s.trigger}),e.jsx("td",{children:e.jsx(S,{tone:js(s.status),children:s.status})}),e.jsxs("td",{className:"muted",children:[Ds(s),s.error&&es(s.counts)?e.jsx("span",{className:"table-sub",children:es(s.counts)}):null]}),e.jsx("td",{className:"num muted",children:i(((a=s.counts)==null?void 0:a.processed)??0)}),e.jsx("td",{className:"num muted",children:i(((l=s.counts)==null?void 0:l.changed)??0)}),e.jsx("td",{className:"num muted",children:i(((r=s.counts)==null?void 0:r.skipped)??0)}),e.jsx("td",{className:"num muted",children:i(((c=s.counts)==null?void 0:c.failed)??0)}),e.jsx("td",{className:"num muted",children:we(s.durationMs)})]})}function za({id:s}){switch(s){case"mdblist":return e.jsx(Da,{});case"sonarr":return e.jsx(Oa,{});case"radarr":return e.jsx(Pa,{});default:return null}}function Ka(){var _,se,ne,te;const{status:s,error:n,loading:t,reload:i}=ve(),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(""),[o,x]=f.useState(!1),[j,u]=f.useState(!1),[v,b]=f.useState(!1),[h,p]=f.useState("23:00"),[w,g]=f.useState("07:00"),[m,R]=f.useState(""),[k,E]=f.useState(!1),L=!!((_=s==null?void 0:s.maintenance)!=null&&_.enabled);f.useEffect(()=>{var O;!j&&s&&d(((O=s.maintenance)==null?void 0:O.message)??"")},[s,j]),f.useEffect(()=>{k||!(s!=null&&s.quietTime)||(b(s.quietTime.enabled),p(s.quietTime.startTime),g(s.quietTime.endTime),R(s.quietTime.message))},[s,k]);const A=O=>r(O?"on":"off",async()=>{await a(()=>q.post("/admin/api/maintenance",{enabled:O,message:c}),O?"Memby is offline for every television.":"Memby is back online."),x(!1),u(!1),await i()}),N=()=>r("quiet",async()=>{await a(()=>q.post("/admin/api/quiet-time",{enabled:v,startTime:h,endTime:w,message:m}),v?"Quiet time saved.":"Quiet time turned off."),E(!1),await i()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time."}),e.jsx(U,{message:n}),t?e.jsx(H,{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:L?"bad":"warn",actions:L?e.jsx(S,{tone:"bad",children:"offline"}):e.jsx(S,{tone:"ok",children:"online"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"danger",disabled:L,onClick:()=>x(!0),children:"Go offline"}),e.jsx($,{disabled:!L,busy:l==="off",onClick:()=>void A(!1),children:"Bring back online"})]}),children:e.jsx(D,{label:"Message shown on the television",hint:"Say what is happening and when it will be back. It is the only thing the viewer is told.",children:e.jsx("input",{type:"text",value:c,placeholder:"Back shortly — upgrading the server",onChange:O=>{d(O.target.value),u(!0)}})})}),t?null:e.jsxs(T,{title:"Quiet time",intro:`Pause new television requests and server background work every day in ${((se=s==null?void 0:s.quietTime)==null?void 0:se.timeZone)??"the server timezone"}. Work already under way finishes safely. The admin console and health checks stay available so the schedule can always be changed.`,icon:"clock",tone:(ne=s==null?void 0:s.quietTime)!=null&&ne.active?"warn":"info",actions:(te=s==null?void 0:s.quietTime)!=null&&te.active?e.jsx(S,{tone:"warn",children:"active now"}):v?e.jsx(S,{tone:"ok",children:"scheduled"}):e.jsx(S,{children:"off"}),footer:e.jsx($,{variant:"primary",busy:l==="quiet",onClick:()=>void N(),children:"Save quiet time"}),children:[e.jsx(Y,{label:"Pause server activity during quiet time",checked:v,onChange:O=>{b(O),E(!0)}}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts",hint:"Uses the server's 24-hour clock.",children:e.jsx("input",{type:"time",value:h,onChange:O=>{p(O.target.value),E(!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:w,onChange:O=>{g(O.target.value),E(!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:m,placeholder:"Quiet time — try again after 7 am",onChange:O=>{R(O.target.value),E(!0)}})})]}),o?e.jsx(Se,{title:"Take Memby offline?",body:"Every television will stop working immediately and show your message in place of the launcher. This console keeps working.",confirmLabel:"Go offline",destructive:!0,busy:l==="on",onConfirm:()=>void A(!0),onCancel:()=>x(!1)}):null]})}const an=s=>s==="Unattributed"?"warn":s==="Go runtime"?"note":"info";function Ga(){const s=Q("/admin/api/runtime",{pollMs:3e4}),[n,t]=f.useState(),[i,a]=f.useState(!1),[l,r]=f.useState(""),c=f.useCallback(async()=>{a(!0),r("");try{t(await q.get("/admin/api/runtime/goroutines"))}catch(b){r(b instanceof Error?b.message:String(b))}finally{a(!1)}},[]);if(s.loading||!s.data)return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Runtime",intro:"What the gateway process is doing."}),e.jsx(U,{message:s.error}),e.jsx(H,{})]});const d=s.data,o=d.memory,x=d.samples??[],j=d.workers??[],u=j.filter(b=>b.state==="running").length,v=o.memoryLimit>0&&o.memoryLimity(Math.round(b)))}`,value:y(d.goroutines),icon:"pulse",tone:d.goroutineTrend.direction==="rising"?"warn":"info"},{label:"named workers running",value:y(u),icon:"clock",tone:"note"},{label:`heap in use · ${cs(d.heapTrend,me)}`,value:me(o.heapInuse),icon:"chip",tone:o.heapShare>=.75?"warn":"data"},{label:"running for",value:_n(d.uptimeSeconds),icon:"history",tone:"data"}]}),e.jsxs(pe,{cols:"2",children:[e.jsxs(T,{title:"Memory",intro:"Three different figures that are routinely confused. Heap in use is what the gateway is actually holding. Reserved is what the Go runtime has taken from the operating system on its behalf — always larger, and not a leak. The limit is the one the container is stopped at.",icon:"chip",tone:"data",children:[v?e.jsxs(e.Fragment,{children:[e.jsx(Fn,{value:o.heapInuse,total:o.memoryLimit,tone:o.heapShare>=.9?"bad":o.heapShare>=.75?"warn":"ok"}),e.jsxs("p",{className:"hint",children:["The heap is using ",(o.heapShare*100).toFixed(1),"% of the"," ",me(o.memoryLimit)," limit",o.configuredLimit?` set by GOMEMLIMIT=${o.configuredLimit}`:"","."]})]}):e.jsx(je,{tone:"warn",children:"No memory limit is set, so the Go runtime will grow until the container is killed by the host. Set GOMEMLIMIT to give it a ceiling to collect against."}),e.jsx(Ve,{rows:[{label:"Heap currently used",value:me(o.heapInuse)},{label:"Heap allocated to live objects",value:me(o.heapAlloc)},{label:"Heap held but idle",value:me(o.heapIdle)},{label:"Returned to the operating system",value:me(o.heapReleased)},{label:"Goroutine stacks",value:me(o.stackInuse)},{label:"Runtime / system reserved",value:me(o.sys)},{label:"Configured process limit",value:v?me(o.memoryLimit):"none"},{label:"Next collection at",value:me(o.nextGc)}]})]}),e.jsxs(T,{title:"Collection",intro:"How often the garbage collector runs and how long it stops the gateway for. Everything the televisions ask for waits during a pause, so this is the figure that turns a memory problem into a slow one.",icon:"refresh",tone:"note",children:[e.jsx(Ls,{tiles:[{label:"collections",value:y(o.numGc)},{label:"collections an hour",value:o.gcPerHour>0?o.gcPerHour.toFixed(0):"—"},{label:"recent pause",value:`${o.pauseRecentMs.toFixed(1)} ms`},{label:"paused in total",value:`${(o.pauseTotalMs/1e3).toFixed(1)} s`}]}),e.jsx(Ve,{rows:[{label:"Processors available",value:y(d.gomaxprocs)},{label:"Operating-system threads",value:y(d.threads)},{label:"Processor use",value:d.process.cpuKnown?`${(d.process.cpuPercent??0).toFixed(1)}% of one processor · ${(d.process.cpuSeconds??0).toFixed(0)}s used in total`:"not available on this host"},{label:"Open connections",value:d.process.filesKnown?`${y(d.process.openSockets)} sockets of ${y(d.process.openFiles)} open files${d.process.fileLimit?` · limit ${y(d.process.fileLimit)}`:""}`:"not available on this host"},{label:"Go version",value:d.goVersion}]})]})]}),e.jsxs(pe,{cols:"2",children:[e.jsx(T,{title:"Goroutines over time",intro:"A single instantaneous count cannot show a leak. This can: a line that climbs and never comes back down is work the gateway is not letting go of.",icon:"chart",tone:"info",children:e.jsx(rn,{samples:x,valueOf:b=>b.goroutines,format:b=>y(b),everySeconds:d.sampleEverySeconds})}),e.jsx(T,{title:"Memory over time",intro:"Heap in use, sampled on the same tick. A saw-tooth that returns to roughly the same floor after each collection is healthy; a floor that keeps rising is not.",icon:"chart",tone:"data",children:e.jsx(rn,{samples:x,valueOf:b=>b.heapInuse,format:me,everySeconds:d.sampleEverySeconds})})]}),e.jsx(T,{title:"Background workers",intro:"Memby's own long-running work, named where it is started rather than guessed at from a stack. A worker that has finished is not necessarily a fault — several are one-shot startup jobs — but one that keeps being started again is failing at something.",icon:"clock",tone:"note",children:e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Worker"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Since"}),e.jsx("th",{className:"num",children:"Starts"})]})}),e.jsx("tbody",{children:j.length===0?e.jsx(X,{columns:5,children:"Nothing has registered. This gateway predates named workers."}):j.map(b=>e.jsx(Za,{worker:b},b.name))})]})})}),e.jsxs(T,{title:"Goroutine breakdown",intro:"What those goroutines are actually doing, and which part of Memby they belong to. Collecting it means walking every stack, which stops the gateway for a few milliseconds — so it is taken when you ask rather than continuously.",icon:"search",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx($,{onClick:c,busy:i,icon:"refresh",variant:"primary",children:n?"Collect again":"Collect breakdown"}),e.jsx("a",{href:"/admin/api/runtime/goroutines?format=text",download:!0,children:e.jsx($,{icon:"download",variant:"quiet",children:"Download full stack dump"})})]}),e.jsx(U,{message:l}),n?e.jsx(Ya,{report:n}):e.jsx(ae,{children:"Nothing collected yet. The snapshot is a moment in time, so it is taken on request and stamped with when it was taken."})]})]})}function Za({worker:s}){const n=s.state==="finished";return e.jsxs("tr",{children:[e.jsx("td",{children:s.name}),e.jsx("td",{children:e.jsx(S,{tone:"info",children:s.component})}),e.jsx("td",{children:e.jsx(S,{tone:n?"note":"ok",children:n?"finished":"running"})}),e.jsx("td",{children:P(n?s.stopped:s.started)}),e.jsx("td",{className:"num",children:s.starts>1?e.jsx(S,{tone:"warn",children:s.starts}):s.starts})]})}function rn({samples:s,valueOf:n,format:t,everySeconds:i}){if(s.length<2)return e.jsxs(ae,{children:["Not enough history yet. Readings are taken every"," ",i>=60?`${Math.round(i/60)} minutes`:`${i}s`,", and a trend needs about fifteen minutes of them."]});const a=l=>new Date(l.at).toLocaleTimeString();return e.jsx(xs,{data:s,labelOf:l=>a(l),valueOf:l=>n(l),title:l=>`${a(l)}: ${t(n(l))}`})}function Ya({report:s}){const n=s.categories??[],t=s.components??[],i=s.groups??[];return e.jsxs(e.Fragment,{children:[e.jsxs("p",{className:"hint",children:[y(s.total)," goroutines, collected ",P(s.at)," in"," ",s.collectedInMs.toFixed(1)," ms."]}),e.jsxs(pe,{cols:"2",children:[e.jsxs("div",{children:[e.jsx(bs,{children:"By what they are doing"}),e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"State"}),e.jsx("th",{className:"num",children:"Count"})]})}),e.jsx("tbody",{children:n.map(a=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:a.label}),e.jsx("p",{className:"hint",children:a.description}),e.jsx("p",{className:"hint",children:(a.states??[]).map(l=>`${l.state} (${l.count})`).join(" · ")})]}),e.jsx("td",{className:"num",children:y(a.count)})]},a.category))})]})})]}),e.jsxs("div",{children:[e.jsx(bs,{children:"By which part of Memby"}),e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Area"}),e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:t.map(a=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(S,{tone:an(a.component),children:a.component})}),e.jsx("td",{className:"num",children:y(a.count)}),e.jsx("td",{className:"num",children:a.longestWaitMinutes>0?`${y(a.longestWaitMinutes)} min`:"—"})]},a.component))})]})})]})]}),e.jsx(bs,{aside:s.groupsTotal>i.length?`the ${i.length} largest of ${y(s.groupsTotal)}`:void 0,children:"Where they are waiting"}),e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Function"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(X,{columns:5,children:"Nothing to group."}):i.map((a,l)=>e.jsxs("tr",{children:[e.jsx("td",{className:"num",children:y(a.count)}),e.jsx("td",{children:e.jsx(S,{tone:an(a.component),children:a.component})}),e.jsx("td",{children:a.state}),e.jsxs("td",{children:[e.jsx("span",{className:"mono",children:a.function}),e.jsx("p",{className:"hint",children:a.file})]}),e.jsx("td",{className:"num",children:a.longestWaitMinutes>0?`${y(a.longestWaitMinutes)} min`:"—"})]},`${a.component}-${a.function}-${a.state}-${l}`))})]})})]})}const Ja=-1;function Oe(s){return s===0?"":s<0?"off":String(s)}function Pe(s,n){const t=s.trim().toLowerCase();if(t==="")return 0;if(n&&(t==="off"||t==="none"||t==="0"))return Ja;const i=Number.parseInt(t,10);return Number.isFinite(i)?i:0}function _e(s,n){return s<=0?"off":`${s} ${n}${s===1?"":"s"}`}function ln(s){switch(s){case"everywhere":return"Everywhere";case"off":return"Off";default:return"Home only"}}function ws(s){return{timezone:s.timezone??"",logLevel:s.logLevel??"",sessionIdleDays:Oe(s.sessionIdleDays),sonarrAlertMinutes:Oe(s.sonarrAlertMinutes),radarrAlertMinutes:Oe(s.radarrAlertMinutes),notificationDisplay:s.notificationDisplay||"home_only",embyHealthSeconds:Oe(s.embyHealthSeconds),slowRequestMillis:Oe(s.slowRequestMillis),librarySyncMinutes:Oe(s.librarySyncMinutes)}}function Qa(){const{data:s,error:n,loading:t,reload:i}=Q("/admin/api/gateway-settings"),{wrap:a}=le(),{busy:l,run:r}=ie(),[c,d]=f.useState(null);f.useEffect(()=>{!c&&s&&d(ws(s.settings))},[s,c]);const o=(p,w)=>d(g=>g&&{...g,[p]:w}),x=()=>r("save",async()=>{if(!c)return;const p={timezone:c.timezone.trim(),logLevel:c.logLevel.trim(),sessionIdleDays:Pe(c.sessionIdleDays,!1),sonarrAlertMinutes:Pe(c.sonarrAlertMinutes,!0),radarrAlertMinutes:Pe(c.radarrAlertMinutes,!0),notificationDisplay:c.notificationDisplay,embyHealthSeconds:Pe(c.embyHealthSeconds,!0),slowRequestMillis:Pe(c.slowRequestMillis,!0),librarySyncMinutes:Pe(c.librarySyncMinutes,!0)},w=await a(()=>q.post("/admin/api/gateway-settings",p),"Gateway settings saved.");w&&d(ws(w.settings)),await i()}),j=()=>r("clear",async()=>{const p=await a(()=>q.post("/admin/api/gateway-settings",{timezone:"",logLevel:"",sessionIdleDays:0,sonarrAlertMinutes:0,radarrAlertMinutes:0,embyHealthSeconds:0,slowRequestMillis:0,librarySyncMinutes:0,notificationDisplay:"home_only"}),"Every setting is back to its deployed or server default.");p&&d(ws(p.settings)),await i()}),u=s==null?void 0:s.deployed,v=s==null?void 0:s.effective,b=(s==null?void 0:s.logLevels)??[],h=(s==null?void 0:s.notificationDisplays)??["everywhere","home_only","off"];return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Gateway settings",intro:"Server-level settings for this gateway, changeable without a redeployment."}),e.jsx(U,{message:n}),t||!c||!u||!v?e.jsx(H,{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(S,{tone:"info",children:(s==null?void 0:s.version)??"unknown"}),children:e.jsx(Ve,{rows:[{label:"Server timezone",value:v.timezone||"not set"},{label:"Log level",value:v.logLevel},{label:"Sign-in expiry",value:_e(v.sessionIdleDays,"day")},{label:"Emby health probe",value:_e(v.embyHealthSeconds,"second")},{label:"Slow-request breakdown",value:_e(v.slowRequestMillis,"millisecond")},{label:"Catalogue sweep",value:_e(v.librarySyncMinutes,"minute")},{label:"Episode alert window",value:_e(v.sonarrAlertMinutes,"minute")},{label:"Film alert window",value:_e(v.radarrAlertMinutes,"minute")},{label:"Notification display",value:ln(v.notificationDisplay)}]})}),e.jsxs(T,{title:"Server settings",intro:"Environment-backed fields can be left empty to use the deployed value shown beneath them. Changes take effect immediately — nothing here needs a restart.",icon:"sliders",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:l==="save",onClick:()=>void x(),children:"Save settings"}),e.jsx($,{busy:l==="clear",onClick:()=>void j(),children:"Use defaults"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Server timezone",hint:`Deployed: ${u.timezone||"not set"}. An IANA name, for example Pacific/Auckland. Decides what "today" means for the schedule rows, the home hero and the sign-in history.`,children:e.jsx("input",{type:"text",value:c.timezone,placeholder:u.timezone,onChange:p=>o("timezone",p.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:c.logLevel,onChange:p=>o("logLevel",p.target.value),children:[e.jsxs("option",{value:"",children:["Deployed (",u.logLevel,")"]}),b.map(p=>e.jsx("option",{value:p,children:p},p))]})})]}),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:c.sessionIdleDays,placeholder:String(u.sessionIdleDays),onChange:p=>o("sessionIdleDays",p.target.value)})}),e.jsx(D,{label:"Slow-request breakdown above (ms)",hint:`Deployed: ${u.slowRequestMillis||"off"}. A request slower than this logs where its time went — Emby, Postgres, Redis, row assembly. Lower it while chasing one slow screen; type off to stop annotating altogether.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:c.slowRequestMillis,placeholder:String(u.slowRequestMillis),onChange:p=>o("slowRequestMillis",p.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:c.embyHealthSeconds,placeholder:String(u.embyHealthSeconds),onChange:p=>o("embyHealthSeconds",p.target.value)})})]}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Catalogue sweep (minutes)",hint:`Deployed: ${u.librarySyncMinutes||"off"}. How often the gateway asks Emby what has changed. With the Sonarr and Radarr webhooks wired up a new file is in the catalogue within a minute of landing, and this is only reconciliation for media they do not manage — 360 is a sensible choice then. Without them it is the only way anything is found.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:c.librarySyncMinutes,placeholder:String(u.librarySyncMinutes),onChange:p=>o("librarySyncMinutes",p.target.value)})})}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Notification display",hint:"Controls informational banners for every viewer and television. Everywhere also permits playback overlays; Home only keeps them off active movies, episodes and trailers; Off hides them throughout Memby.",children:e.jsx("select",{value:c.notificationDisplay,onChange:p=>o("notificationDisplay",p.target.value),children:h.map(p=>e.jsx("option",{value:p,children:ln(p)},p))})})}),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:c.sonarrAlertMinutes,placeholder:String(u.sonarrAlertMinutes),onChange:p=>o("sonarrAlertMinutes",p.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:c.radarrAlertMinutes,placeholder:String(u.radarrAlertMinutes),onChange:p=>o("radarrAlertMinutes",p.target.value)})})]}),e.jsxs(je,{tone:"note",children:["These settings live in the database and survive a restart. A deployment rewrites",e.jsx("code",{children:".env"}),", not this document, so environment-backed values can then disagree; permanent environment changes belong in ",e.jsx("code",{children:".env.example"})," as well."]}),s!=null&&s.settings.updatedBy?e.jsxs(je,{children:["Last changed by ",s.settings.updatedBy,s.settings.updatedAt?` on ${new Date(s.settings.updatedAt).toLocaleString("en-NZ")}`:"","."]}):null]})]})]})}const Xa={done:"ok",pending:"warn",failed:"bad"};function ei(s){const n=s.payload??{};if(n.series){const t=n.episode&&n.episode>0?` S${String(n.season??0).padStart(2,"0")}E${String(n.episode).padStart(2,"0")}`:"";return`${n.series}${t}`}return n.title?n.year?`${n.title} (${n.year})`:n.title:s.key}function si(){const{data:s,error:n,loading:t}=Q("/admin/api/ingest",{pollMs:15e3});if(t)return e.jsx(H,{rows:1});const i=!!(s!=null&&s.sonarrConfigured||s!=null&&s.radarrConfigured),a=(s==null?void 0:s.recent)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{message:n}),e.jsxs(T,{title:"Webhook activity",intro:"Sonarr and Radarr are what put files on disk, so they are what the catalogue learns from. A notification is recorded the moment it arrives and read into the catalogue once the file has settled — which is why an import appears here before it appears in Emby.",icon:"plug",tone:"info",children:[e.jsx(ce,{tiles:[{label:"Sonarr webhook",value:s!=null&&s.sonarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.sonarrConfigured?"ok":"warn",icon:"tv"},{label:"Radarr webhook",value:s!=null&&s.radarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.radarrConfigured?"ok":"warn",icon:"play"},{label:"Waiting",value:y((s==null?void 0:s.counts.pending)??0),icon:"clock",tone:"note"},{label:"Given up on",value:y((s==null?void 0:s.counts.failed)??0),icon:"alert",tone:"bad"},{label:"Settle delay",value:`${(s==null?void 0:s.settleSeconds)??0}s`,small:!0,icon:"history",tone:"data"}]}),i?null:e.jsxs("p",{className:"muted",children:["Neither hook has a token, so both answer 404 and nothing is recorded here. Set MEMBY_SONARR_WEBHOOK_TOKEN and MEMBY_RADARR_WEBHOOK_TOKEN, then point each *arr at"," ",e.jsx("code",{children:"/hooks/sonarr"})," and ",e.jsx("code",{children:"/hooks/radarr"}),". Until then the catalogue sweep below is the only way a new title is found."]}),e.jsx(G,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"What"}),e.jsx("th",{children:"Why"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Tries"}),e.jsx("th",{children:"Notes"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(X,{columns:8,children:i?"Nothing has been imported, upgraded, renamed or deleted since this was switched on.":"No webhook is configured."}):a.map(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:P(l.updatedAt)}),e.jsx("td",{className:"muted",children:l.source||"—"}),e.jsx("td",{children:ei(l)}),e.jsx("td",{className:"muted",children:l.reason}),e.jsx("td",{children:e.jsx(S,{tone:Xa[l.state]??"warn",children:l.state})}),e.jsx("td",{className:"muted",children:l.outcome||"—"}),e.jsx("td",{className:"num",children:l.attempts}),e.jsx("td",{className:"muted",children:l.lastError||""})]},l.key))})]})})]})]})}function ni(){const{status:s,error:n,loading:t}=ve(),i=(s==null?void 0:s.runs)??[];return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Imports",intro:"What Sonarr and Radarr said changed, and the catalogue sweep that reconciles everything they do not manage."}),e.jsx(U,{message:n}),e.jsx(si,{}),t?e.jsx(H,{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. With both webhooks wired up this is reconciliation — media dropped in by hand, a title edited in Emby, a notification that never arrived — rather than how new titles are found.",icon:"sync",tone:"data",children:e.jsx(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(X,{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(S,{tone:a.status==="success"?"ok":a.status==="running"?"warn":"bad",children:a.status})}),e.jsx("td",{className:"num",children:y(a.itemsSeen)}),e.jsx("td",{className:"num",children:y(a.itemsUpserted)}),e.jsx("td",{className:"num",children:y(a.itemsRemoved)}),e.jsx("td",{className:"muted",children:a.error||""})]},a.id||a.startedAt))})]})})})]})}const ti={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"},Wn=s=>ti[s]??"quiet",ai={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"]},ii=[[/^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 ri(s,n){const t=n.toLowerCase();for(const[a,l]of ii)if(a.test(t))return l;const i=ai[s];return i||(s?["gateway","Gateway",gs(s.replace(/[-_]/g," "))]:["gateway","Gateway","Server"])}const li={h:36e5,m:6e4,s:1e3,ms:1,us:.001,µs:.001,ns:1e-6};function Ms(s){if(typeof s=="number")return Number.isFinite(s)?s:null;if(typeof s!="string"||!s)return null;const n=s.matchAll(/([0-9]*\.?[0-9]+)(ns|µs|us|ms|h|m|s)/g);let t=0,i=!1;for(const a of n){const l=a[2]?li[a[2]]:void 0;l!==void 0&&(t+=Number(a[1])*l,i=!0)}return i?t:null}function qs(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 oi=s=>s>=3e3?"bad":s>=1e3?"warn":null,ci={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 di(s){const n=ci[s];return n||(s>=500?"Server Error":s>=400?"Client Error":s>=300?"Redirected":s>=200?"OK":"Response")}const hi=s=>s>=500?"bad":s>=400?"warn":s>=300?"quiet":"ok",ui={"/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"},mi=s=>/\d/.test(s)||s.length>24;function on(s){const n=ui[s];if(n)return n;const t=s.split("/").filter(a=>a&&a!=="v1"&&a!=="api");t[0]==="admin"&&t.shift();const i=t.filter(a=>!mi(a));return i.length===0?s:gs(i.join(" ").replace(/[-_.]/g," ").replace(/\s+/g," ").trim())}const gs=s=>s&&s.charAt(0).toUpperCase()+s.slice(1),ts=s=>gs(s.replace(/_/g," ")),be=s=>s==null?"":String(s),Bn=new Set(["","unknown","none","null","","0"]),Re=s=>!Bn.has(be(s).toLowerCase()),pi=["title","series","name","query","item_title","file"],xi={directplay:"ok",direct:"ok",directstream:"ok",transcode:"warn",transcoding:"warn"};function ji(s,n,t){if(t!==null)return{label:`${t} ${di(t)}`,short:String(t),tone:hi(t)};if(Re(s.error))return{label:"Failed",short:"Failed",tone:n==="WARN"?"warn":"bad"};const i=be(s.play_method).toLowerCase().replace(/[\s_-]/g,"");if(i&&!Bn.has(i)){const a=gs(be(s.play_method).replace(/([a-z])([A-Z])/g,"$1 $2"));return{label:a,short:a,tone:xi[i]??"info"}}if(Re(s.cache)){const a=/hit|true|yes/i.test(be(s.cache));return{label:a?"Cached":"Cache miss",short:a?"Cached":"Miss",tone:a?"data":"quiet"}}return n==="ERROR"?{label:"Failed",short:"Failed",tone:"bad"}:n==="WARN"?{label:"Warning",short:"Warning",tone:"warn"}:null}function gi(s){const n=[];Re(s.user)&&n.push(be(s.user)),Re(s.device)&&n.push(be(s.device));const t=Ms(s.marker_ms);t!==null&&t>0&&n.push(`Start ${cn(t)}`);const i=Ms(s.position);return i!==null&&i>0&&n.push(`At ${cn(i)}`),Re(s.watched)&&n.push(`${be(s.watched)} watched`),Re(s.reason)&&n.push(be(s.reason)),n.slice(0,3).join(" · ")}function cn(s){const n=Math.round(s/1e3),t=Math.floor(n/3600),i=Math.floor(n%3600/60),a=n%60,l=r=>String(r).padStart(2,"0");return t>0?`${t}:${l(i)}:${l(a)}`:`${i}:${l(a)}`}const Vn=[{title:"Request",keys:["method","path","query_keys","status","cache","client","protocol","host"]},{title:"Context",keys:["user","user_id","device","device_id","item","title","series","type","play_method","play_session_id","media_source_id","position","resume","runtime","watched","subtitles","subtitle_track","subtitle_language","event_name"]},{title:"Where the time went",keys:["duration","breakdown","elsewhere"]},{title:"Diagnostics",keys:["error","stack","correlation","version","gateway_version"]}],vi=new Set(Vn.flatMap(s=>s.keys)),fi=s=>vi.has(s),bi=new Intl.DateTimeFormat("en-NZ",{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),yi=new Intl.DateTimeFormat("en-NZ",{weekday:"short",day:"numeric",month:"short"}),dn=new WeakMap;function Xe(s){const n=dn.get(s);if(n)return n;const t=wi(s);return dn.set(s,t),t}function wi(s){const n=s.attributes??{},t=s.message??"",[i,a,l]=ri(be(n.component),t),r=be(n.path),c=r?be(n.method).toUpperCase():"",d=Number(n.status),o=r&&Number.isFinite(d)&&d>0?d:null,x=t==="request"&&!!r,j=pi.find(N=>Re(n[N])),u=j?be(n[j]):"";let v,b;x?(v=c||"HTTP",b=on(r)):r&&c?(v=c,b=u?`${ts(t)} · ${u}`:`${ts(t)} — ${on(r)}`):(v="",b=u?`${ts(t)} · ${u}`:ts(t));const h=Ms(n.duration??n.duration_ms??n.negotiation_duration),p=Re(n.error)?be(n.error):"",w=gi(n),g=new Date(s.occurredAt),m=be(n.breakdown),R=p||(x?m:w),k=p?"error":R?"context":"",E=!p&&x?w:"",L=Object.entries(n);return{serviceKey:i,service:a,component:l,action:v,summary:b,context:w,detail:p,secondary:R,secondaryTone:k,trail:E,result:ji(n,s.level,o),durationMs:h,method:c,status:o,eventKey:t,level:s.level,time:`${bi.format(g)}.${String(g.getMilliseconds()).padStart(3,"0")}`,day:yi.format(g),dayKey:g.toDateString(),tall:!!R,haystack:[t,a,l,b,w,p,...L.flat().map(be)].join(" ").toLowerCase(),fields:L,attributes:n}}const hn={TRACE:5,DEBUG:10,INFO:20,WARN:30,ERROR:40},as={level:"INFO",service:"",component:"",event:"",method:"",status:"",slower:0,text:""};function ki(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 Ni(s,n,t){if((hn[s.level]??0)<(hn[n.level]??20))return!1;const i=Xe(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||!ki(n.status,i.status)||n.slower>0&&(i.durationMs??0)({key:l,label:r})).sort((l,r)=>l.label.localeCompare(r.label)),components:[...t].sort((l,r)=>l.localeCompare(r)),events:[...i].sort((l,r)=>l.localeCompare(r)),methods:[...a].sort((l,r)=>l.localeCompare(r))}}function Ci(s,n){var i;const t=[];if(s.service){const a=((i=n.find(l=>l.key===s.service))==null?void 0:i.label)??s.service;t.push({key:"service",label:`Service: ${a}`})}return s.component&&t.push({key:"component",label:`Component: ${s.component}`}),s.event&&t.push({key:"event",label:`Event: ${s.event}`}),s.method&&t.push({key:"method",label:`Method: ${s.method}`}),s.status&&t.push({key:"status",label:`Status: ${s.status==="error"?"≥400":s.status}`}),s.slower>0&&t.push({key:"slower",label:`Duration: >${qs(s.slower)}`}),s.text&&t.push({key:"text",label:`Search: ${s.text}`}),t}const is=2e4,Mi=5e3,un=7,Ti=16,$i=15,Ri=2,Ei=1,Ts=un+Ti+un+Ei,mn=Ts+Ri+$i,pn=26,Ai=31,xn=10,Ii=[{value:"TRACE",label:"Everything"},{value:"DEBUG",label:"Debug+"},{value:"INFO",label:"Info+"},{value:"WARN",label:"Warnings+"},{value:"ERROR",label:"Errors only"}],Li=[{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"}],jn=s=>s.replace(/_/g," ");function rs({onPick:s,className:n,title:t,children:i,...a}){return e.jsx("button",{type:"button",className:`logfacet ${n}`,title:t,onClick:s,...a,children:i})}const qi=f.memo(function({event:n,view:t,top:i,height:a,selected:l,onInspect:r,onFilter:c}){const d=t.durationMs!==null?oi(t.durationMs):null;return e.jsxs("div",{className:"logrow","data-level":t.level,"data-selected":l||void 0,style:{transform:`translateY(${i}px)`,height:`${a}px`},children:[e.jsx("time",{className:"logrow-time",title:n.occurredAt,children:t.time}),e.jsx(rs,{className:"logrow-level","data-level":t.level,title:`Show ${t.level} and above`,onPick:()=>c({level:t.level}),children:t.level}),e.jsxs("span",{className:"logrow-place",children:[e.jsx(rs,{className:"logrow-service","data-tone":Wn(t.serviceKey),title:`Filter to ${t.service}`,onPick:()=>c({service:t.serviceKey,component:""}),children:t.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),e.jsx(rs,{className:"logrow-component",title:`Filter to ${t.component}`,onPick:()=>c({component:t.component}),children:t.component})]}),e.jsxs("button",{type:"button",className:"logrow-summary",title:[t.summary,t.trail,t.secondary].filter(Boolean).join(" — "),onClick:()=>r(n.sequence),children:[e.jsxs("span",{className:"logrow-line",children:[t.action?e.jsx("b",{className:"logrow-action","data-method":t.method||void 0,children:t.action}):null,e.jsx("span",{className:"logrow-text",children:t.summary}),t.trail?e.jsx("span",{className:"logrow-trail",children:t.trail}):null]}),t.secondary?e.jsx("span",{className:"logrow-second","data-tone":t.secondaryTone,children:t.secondaryTone==="error"?`↳ ${t.secondary}`:t.secondary}):null]}),e.jsx("span",{className:"logrow-result",children:t.result?e.jsx(rs,{className:"logrow-verdict","data-tone":t.result.tone,title:t.status!==null?`Filter to ${t.status}`:`Filter to ${t.eventKey}`,onPick:()=>t.status!==null?c({status:`${Math.floor(t.status/100)}xx`}):c({event:t.eventKey}),children:t.result.label}):null}),e.jsx("span",{className:"logrow-duration","data-tone":d??void 0,children:t.durationMs!==null?qs(t.durationMs):""})]})});function Fi({event:s,view:n,onClose:t}){const[i,a]=f.useState(!1),l=n.fields.filter(([d])=>!fi(d)&&d!=="component"),r=async()=>{try{await navigator.clipboard.writeText(JSON.stringify(s,null,2)),a(!0),window.setTimeout(()=>a(!1),1600)}catch{a(!1)}},c=Vn.map(d=>({title:d.title,rows:d.keys.map(o=>[o,n.attributes[o]]).filter(([,o])=>o!=null&&String(o)!=="")})).filter(d=>d.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":Wn(n.serviceKey),children:n.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),n.component]}),e.jsx("b",{children:n.summary}),n.detail?e.jsx("p",{className:"logdrawer-error",children:n.detail}):null]}),e.jsxs("div",{className:"logdrawer-actions",children:[e.jsx($,{size:"sm",variant:"quiet",onClick:r,icon:"download",children: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:qs(n.durationMs)})]}):null,e.jsx("dt",{children:"Record"}),e.jsxs("dd",{children:["#",s.sequence]})]})]}),c.map(d=>e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:d.title}),e.jsx("dl",{children:d.rows.map(([o,x])=>e.jsxs(f.Fragment,{children:[e.jsx("dt",{children:jn(o)}),e.jsx("dd",{children:String(x)})]},o))})]},d.title)),l.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Details"}),e.jsx("dl",{children:l.map(([d,o])=>e.jsxs(f.Fragment,{children:[e.jsx("dt",{children:jn(d)}),e.jsx("dd",{children:String(o)})]},d))})]}):null]}),e.jsxs("details",{className:"logdrawer-raw",children:[e.jsx("summary",{children:"Raw event"}),e.jsx("pre",{children:JSON.stringify(s,null,2)})]})]})}function Oi(){var Ke;const[s,n]=f.useState([]),[t,i]=f.useState(0),[a,l]=f.useState(!1),[r,c]=f.useState(0),[d,o]=f.useState(()=>{const M=new URLSearchParams(window.location.search).get("q")??"";return M?{...as,text:M}:as}),[x,j]=f.useState(""),[u,v]=f.useState({top:0,height:600}),[b,h]=f.useState(!0),[p,w]=f.useState(null),g=f.useDeferredValue(d.text.trim().toLowerCase()),m=f.useRef(0),R=f.useRef(!1),k=f.useRef(0),E=f.useRef(null),L=f.useRef(!0),A=f.useRef(void 0),N=f.useRef([]),_=f.useRef(a);f.useEffect(()=>{_.current=a},[a]);const se=f.useCallback(M=>{n(z=>{const ee=z.concat(M);return ee.length>is?ee.slice(ee.length-is):ee})},[]),ne=f.useCallback(async()=>{if(R.current||document.hidden)return;R.current=!0;const M=k.current,z=[];let ee=0;try{let ye=0,Te;do Te=await q.get(`/admin/api/events?after=${m.current}&limit=1000`),m.current=Te.next||m.current,ee+=Te.dropped||0,z.push(...Te.events??[]),ye+=1;while(Te.hasMore&&ye<20);j("")}catch(ye){j(ye instanceof Error?ye.message:String(ye))}finally{z.length>0&&M===k.current&&(_.current?(N.current=N.current.concat(z),N.current.length>is&&(N.current=N.current.slice(N.current.length-is)),c(N.current.length)):se(z)),ee>0&&M===k.current&&i(ye=>ye+ee),R.current=!1}},[se]);f.useEffect(()=>{let M;const z=()=>{window.clearInterval(M),M=document.hidden?void 0:window.setInterval(()=>void ne(),Mi)},ee=()=>{z(),document.hidden||ne()};return ne(),z(),document.addEventListener("visibilitychange",ee),()=>{window.clearInterval(M),document.removeEventListener("visibilitychange",ee)}},[ne]);const te=f.useCallback(()=>{_.current=!0,l(!0)},[]),O=f.useCallback(()=>{_.current=!1;const M=N.current;N.current=[],c(0),l(!1),M.length&&se(M)},[se]),re=f.useCallback(M=>{o(z=>({...z,...M}))},[]),C=f.useMemo(()=>s.filter(M=>Ni(M,d,g)),[s,d,g]),F=f.useMemo(()=>{const M=new Float64Array(C.length+1),z=new Uint8Array(C.length),ee=new Uint8Array(C.length);let ye=0,Te="";for(let Fe=0;Fe{let M=0,z=C.length;for(;M>1;(F.tops[ee]??0)+(F.heights[ee]??0)<=Z?M=ee+1:z=ee}return Math.max(0,M-xn)},[F,Z,C.length]),Ne=f.useMemo(()=>{const M=Z+u.height;let z=he;for(;z{const M=[];for(let z=he;zs.find(M=>M.sequence===p),[s,p]),I=f.useCallback(()=>{const M=E.current;M&&(L.current=!0,M.scrollTop=M.scrollHeight,h(!0),v({top:M.scrollTop,height:M.clientHeight}))},[]);f.useLayoutEffect(()=>{const M=E.current;!M||!L.current||(M.scrollTop=M.scrollHeight,v({top:M.scrollTop,height:M.clientHeight}))},[Ie,F.total]),f.useEffect(()=>()=>window.cancelAnimationFrame(A.current??0),[]);const W=()=>{const M=E.current;if(!M)return;const z=M.scrollHeight-M.scrollTop-M.clientHeight{v({top:M.scrollTop,height:M.clientHeight})})},ue=()=>{const M=new Blob([JSON.stringify(C,null,2)],{type:"application/json"}),z=document.createElement("a");z.href=URL.createObjectURL(M),z.download=`memby-events-${new Date().toISOString().replace(/[:.]/g,"-")}.json`,z.click(),window.setTimeout(()=>URL.revokeObjectURL(z.href),1e3)},xe=f.useMemo(()=>Si(s),[s]),qe=Ci(d,xe.services);return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Server logs",intro:"Structured gateway events as they happen."}),e.jsx(U,{message:x}),e.jsxs(T,{children:[e.jsxs("div",{className:"logbar",children:[e.jsxs("div",{className:"logbar-filters",children:[e.jsxs("select",{"aria-label":"Service",value:d.service,onChange:M=>re({service:M.target.value,component:""}),children:[e.jsx("option",{value:"",children:"All services"}),xe.services.map(M=>e.jsx("option",{value:M.key,children:M.label},M.key))]}),e.jsxs("select",{"aria-label":"Component",value:d.component,onChange:M=>re({component:M.target.value}),children:[e.jsx("option",{value:"",children:"All components"}),xe.components.map(M=>e.jsx("option",{value:M,children:M},M))]}),e.jsx("select",{"aria-label":"Level",value:d.level,onChange:M=>re({level:M.target.value}),children:Ii.map(M=>e.jsx("option",{value:M.value,children:M.label},M.value))}),e.jsxs("select",{"aria-label":"Event",value:d.event,onChange:M=>re({event:M.target.value}),children:[e.jsx("option",{value:"",children:"All events"}),xe.events.map(M=>e.jsx("option",{value:M,children:M},M))]}),e.jsx("select",{"aria-label":"Result",value:d.status,onChange:M=>re({status:M.target.value}),children:Li.map(M=>e.jsx("option",{value:M.value,children:M.label},M.value))}),e.jsxs("select",{"aria-label":"Method",value:d.method,onChange:M=>re({method:M.target.value}),children:[e.jsx("option",{value:"",children:"Any method"}),xe.methods.map(M=>e.jsx("option",{value:M,children:M},M))]}),e.jsx("select",{"aria-label":"Duration",value:String(d.slower),onChange:M=>re({slower:Number(M.target.value)}),children:Di.map(M=>e.jsx("option",{value:String(M.value),children:M.label},M.value))}),e.jsxs("label",{className:"logsearch",children:[e.jsx(oe,{name:"search"}),e.jsx("input",{type:"search",value:d.text,"aria-label":"Search logs",placeholder:"Search person, title, service, component, path, request ID…",onChange:M=>re({text:M.target.value})})]})]}),e.jsxs("div",{className:"logbar-actions",children:[e.jsx($,{size:"sm",variant:"quiet",onClick:()=>a?O():te(),icon:a?"play":"clock",children:a?r?`Resume (${y(r)})`:"Resume":"Pause"}),e.jsx($,{size:"sm",variant:"quiet",onClick:()=>{k.current+=1,N.current=[],c(0),n([]),i(0),w(null)},children:"Clear view"}),e.jsx($,{size:"sm",variant:"quiet",onClick:ue,icon:"download",title:"Export the rows matching the current filters, as delivered by the gateway",children:"Export JSON"})]})]}),qe.length?e.jsxs("div",{className:"logchips",children:[qe.map(M=>e.jsxs("button",{type:"button",className:"logchip",onClick:()=>re({[M.key]:as[M.key]}),children:[M.label,e.jsx(oe,{name:"close"})]},M.key)),e.jsx("button",{type:"button",className:"logchip logchip-clear",onClick:()=>o(as),children:"Clear all"})]}):null,e.jsxs("div",{className:"logshell",children:[e.jsxs("div",{className:"logview",ref:E,onScroll:W,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"})]}),C.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:`${F.total}px`},children:Ae.map(({event:M,view:z,index:ee})=>e.jsxs(f.Fragment,{children:[F.divider[ee]?e.jsx("div",{className:"logday",style:{transform:`translateY(${(F.tops[ee]??0)-pn}px)`},children:e.jsx("span",{children:z.day})}):null,e.jsx(qi,{event:M,view:z,top:F.tops[ee]??0,height:F.heights[ee]??Ts,selected:M.sequence===p,onInspect:w,onFilter:re})]},M.sequence))})]}),!b&&C.length>0?e.jsxs("button",{type:"button",className:"logtail",onClick:I,children:[e.jsx(oe,{name:"caret"}),"Jump to latest"]}):null]}),e.jsxs("p",{className:"hint",children:[y(s.length)," retained · ",y(C.length)," matching",C.length?` · ${y(Ae.length)} rows mounted`:"",t?` · ${y(t)} overwritten before delivery`:"",a?` · paused${r?`, ${y(r)} held`:""}`:""]}),fe?e.jsx(Fi,{event:fe,view:Xe(fe),onClose:()=>w(null)}):s.length?e.jsx(je,{children:"Select a row to see the full record — request, context, diagnostics and raw event."}):null]})]})}function $e(s){const n=String(s||"—").replaceAll("_"," ");return{favorites:"Favourites",continue:"Continue Watching",continue_watching:"Continue Watching",recent_searches:"Recent Searches",genre_browse:"Genre Browse",for_you:"Recommendations",recommendation:"Recommendations",for_you_time:"Recommendation Timing",my_shows:"My Shows",magic_movie:"Magic Movie",detail_page:"Details",home_hero:"Home Hero",screen_view:"Viewed",open:"Opened",close:"Closed",select:"Selected",request:"Requested",start:"Started",stop:"Stopped",complete:"Completed",change:"Changed",toggle:"Toggled",submit:"Submitted",journey_start:"Started Session",journey_end:"Ended Session",abandoned:"Abandoned / interrupted",text:"Text Search",voice:"Voice Search"}[String(s||"")]??n.replace(/\b\w/g,i=>i.toUpperCase())}function Pi(){const[s,n]=f.useState(30),[t,i]=f.useState(""),a=$s(),l=f.useMemo(()=>`/admin/api/journeys${ze({days:s,userId:t})}`,[s,t]),{data:r,error:c,loading:d}=Q(l),o=r==null?void 0:r.stats,x=(r==null?void 0:r.users)??[],j=(r==null?void 0:r.featureUsage)??[],u=(r==null?void 0:r.featureBreakdown)??[],v=(r==null?void 0:r.paths)??[],b=v[0];return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"User journeys",intro:"How viewers move through Memby, use features and complete flows."}),e.jsx(U,{message:c}),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(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:t,onChange:h=>{const p=h.target.value;i(p),p&&a(`/admin/journeys/${encodeURIComponent(p)}`)},children:[e.jsx("option",{value:"",children:"All users"}),x.map(h=>e.jsx("option",{value:h.userId,children:h.username||h.userId},h.userId))]})})]}),children:d?e.jsx(H,{rows:1}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:"journeys",value:y(o==null?void 0:o.journeys),icon:"list",tone:"data"},{label:"viewers",value:y(o==null?void 0:o.viewers),icon:"people",tone:"info"},{label:"completion",value:Je(o==null?void 0:o.completionRate),icon:"check",tone:"ok"},{label:"abandoned",value:y(o==null?void 0:o.abandoned),icon:"alert",tone:"note"},{label:"active now",value:y(o==null?void 0:o.active),icon:"pulse",tone:"info"},{label:"average steps",value:((o==null?void 0:o.averageSteps)??0).toFixed(1),icon:"chart"},{label:"average visit",value:we(o==null?void 0:o.averageTimeMs),small:!0,icon:"clock"},{label:"history kept",value:`${(r==null?void 0:r.retentionDays)??90} days`,small:!0,icon:"clock"}]}),e.jsxs("div",{className:"summary-grid",children:[e.jsxs("div",{className:"summary",children:[e.jsxs("div",{className:"summary-head",children:[e.jsx("b",{children:"Visit completion"}),e.jsx("strong",{children:Je(o==null?void 0:o.completionRate)})]}),e.jsx(Fn,{value:(o==null?void 0:o.completed)??0,total:(o==null?void 0:o.journeys)??0}),e.jsxs("p",{children:[y(o==null?void 0:o.completed)," completed · ",y(o==null?void 0:o.abandoned)," abandoned ·"," ",y(o==null?void 0:o.active)," active"]})]}),e.jsxs("div",{className:"summary",children:[e.jsx("div",{className:"summary-head",children:e.jsx("b",{children:"Most common route"})}),e.jsx("strong",{style:void 0,children:b?`${$e(b.from)} → ${$e(b.to)}`:"Not enough data"}),e.jsx("p",{children:b?`${y(b.count)} times in this window`:"Journeys will appear here as viewers move through Memby."})]})]})]})}),e.jsxs(pe,{cols:"2",children:[e.jsx(T,{title:"Feature usage",intro:"Distinct viewers, total uses and visits containing each feature.",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:"Feature"}),e.jsx("th",{className:"num",children:"Users"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{className:"num",children:"Active users"})]})}),e.jsx("tbody",{children:j.length===0?e.jsx(X,{columns:5,children:"No feature usage in this window."}):j.map(h=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("b",{children:$e(h.feature)})}),e.jsx("td",{className:"num",children:y(h.users)}),e.jsx("td",{className:"num",children:y(h.uses)}),e.jsx("td",{className:"num",children:y(h.journeys)}),e.jsx("td",{className:"num",children:Je(h.activeUserRate)})]},h.feature))})]})})}),e.jsx(T,{title:"Feature and action detail",intro:"Structured sub-features show which parts of a feature were used.",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:"Feature / sub-feature"}),e.jsx("th",{children:"Action"}),e.jsx("th",{className:"num",children:"Users"}),e.jsx("th",{className:"num",children:"Times"}),e.jsx("th",{className:"num",children:"Visits"})]})}),e.jsx("tbody",{children:u.length===0?e.jsx(X,{columns:5,children:"No feature details in this window."}):u.slice(0,30).map((h,p)=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:$e(h.feature)}),e.jsx("span",{className:"table-sub",children:$e(h.subFeature)})]}),e.jsx("td",{children:$e(h.action)}),e.jsx("td",{className:"num",children:y(h.users)}),e.jsx("td",{className:"num",children:y(h.uses)}),e.jsx("td",{className:"num",children:y(h.journeys)})]},`${h.feature}:${h.subFeature}:${h.action}:${p}`))})]})})})]}),e.jsx(T,{title:"Most common routes",intro:"The most common steps between screens, including where quiet visits ended.",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:"Route"}),e.jsx("th",{className:"num",children:"Times"})]})}),e.jsx("tbody",{children:v.length===0?e.jsx(X,{columns:2,children:"No repeated paths in this window."}):v.map((h,p)=>e.jsxs("tr",{children:[e.jsxs("td",{children:[$e(h.from)," ",e.jsx("span",{className:"route-arrow",children:"→"})," ",$e(h.to)]}),e.jsx("td",{className:"num",children:y(h.count)})]},`${h.from}:${h.to}:${p}`))})]})})}),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 ke=s=>{const n=String(s||"—").replaceAll("_"," ");return{favorites:"Favourites",continue_watching:"Continue Watching",recommendation:"Recommendations",for_you:"Recommendations",screen_view:"Viewed",open:"Opened",close:"Closed",select:"Selected",request:"Requested",start:"Started",stop:"Stopped",complete:"Completed",text:"Text Search",voice:"Voice Search",journey_start:"Started Session",journey_end:"Ended Session"}[String(s||"")]??n.replace(/\b\w/g,i=>i.toUpperCase())},Hn=s=>ke((s==null?void 0:s.target)||(s==null?void 0:s.screen)||(s==null?void 0:s.source)||(s==null?void 0:s.feature)),_i=s=>{const n=s.find(t=>t.category==="playback"&&t.action==="request");return n!=null&&n.source?ke(n.source):Hn(s[0])},gn=s=>s.itemName?`${ke(s.itemType)} · ${s.itemName}`:s.source&&s.target?`${ke(s.source)} → ${ke(s.target)}`:Hn(s),Ui=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]??ke(s.action);function Wi(s){var i;const n=[...s].reverse().find(a=>a.category==="playback"&&a.outcome);if((n==null?void 0:n.outcome)==="failure")return{label:"playback failed",tone:"warn"};if((n==null?void 0:n.outcome)==="completed")return{label:"watched",tone:"ok"};if((n==null?void 0:n.outcome)==="abandoned")return{label:"stopped part-way",tone:"note"};if((n==null?void 0:n.outcome)==="success")return{label:"watched",tone:"ok"};const t=(i=[...s].reverse().find(a=>a.outcome))==null?void 0:i.outcome;return t==="success"||t==="completed"?{label:ke(t),tone:"ok"}:t==="failure"||t==="cancelled"||t==="abandoned"?{label:ke(t),tone:"note"}:s.some(a=>a.action==="stop"&&a.category==="playback")?{label:"watched",tone:"ok"}:{label:"left before playback ended",tone:"warn"}}function Bi(s){const n=s.reduce((t,i,a)=>(i.category==="playback"&&i.action==="request"&&t.push(a),t),[]);return n.length===0?[s]:n.map((t,i)=>s.slice(i===0?0:t,n[i+1]??s.length))}function Vi(){var x,j;const{userId:s=""}=He(),n=f.useMemo(()=>`/admin/api/journeys${ze({days:90,userId:s})}`,[s]),{data:t,error:i,loading:a}=Q(n),l=((j=(x=t==null?void 0:t.users)==null?void 0:x.find(u=>u.userId===s))==null?void 0:j.username)||s,r=f.useMemo(()=>{const u=new Map;for(const v of(t==null?void 0:t.events)??[])u.set(v.journeyId,[...u.get(v.journeyId)??[],v]);return[...u.values()].map(v=>v.sort((b,h)=>b.sequence-h.sequence)).sort((v,b)=>{var h,p;return(((h=b[0])==null?void 0:h.occurredAt)??"").localeCompare(((p=v[0])==null?void 0:p.occurredAt)??"")})},[t==null?void 0:t.events]),c=r.flatMap(u=>Bi(u).map((v,b)=>{var h;return{events:v,key:`${(h=u[0])==null?void 0:h.journeyId}:${b}`}})),d=(t==null?void 0:t.featureUsage)??[],o=(t==null?void 0:t.featureBreakdown)??[];return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:`${l}'s journeys`,intro:"Each app session is shown as the viewing journeys it contains: entry, selection, playback outcome.",icon:"journey",crumbs:e.jsx(J,{className:"crumb",to:"/admin/journeys",children:"Journeys"})}),e.jsx(U,{message:i}),!a&&e.jsxs(T,{title:"Feature usage summary",intro:"This viewer’s feature use before the detailed journeys below.",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:"Feature"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{children:"Last used"})]})}),e.jsx("tbody",{children:d.length===0?e.jsx("tr",{children:e.jsx("td",{colSpan:4,className:"empty",children:"No feature usage recorded for this viewer."})}):d.map(u=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:ke(u.feature)}),e.jsxs("span",{className:"table-sub",children:[y(u.users)," viewer"]})]}),e.jsx("td",{className:"num",children:y(u.uses)}),e.jsx("td",{className:"num",children:y(u.journeys)}),e.jsx("td",{children:P(u.lastUsedAt)})]},u.feature))})]})}),o.length>0&&e.jsxs("p",{className:"table-sub",children:["Breakdown: ",o.slice(0,8).map(u=>`${ke(u.feature)} · ${ke(u.subFeature)} · ${ke(u.action)} (${y(u.uses)})`).join(" · ")]})]}),a?e.jsx(H,{}):e.jsx(T,{title:"Viewing journeys",intro:`${r.length} app session${r.length===1?"":"s"} · ${c.length} viewing journey${c.length===1?"":"s"} in the last 90 days.`,icon:"journey",tone:"info",children:e.jsx("div",{className:"visits",children:c.length===0?e.jsx("p",{className:"empty",children:"No journeys recorded for this viewer."}):c.map((u,v)=>{const b=u.events,h=b[0],p=[...b].reverse().find(g=>g.itemName||g.action==="select"||g.category==="playback"&&g.action==="request"),w=Wi(b);return e.jsxs("article",{className:"visit",children:[e.jsxs("header",{children:[e.jsxs("div",{children:[e.jsx("b",{children:P(h==null?void 0:h.occurredAt)}),e.jsxs("span",{children:["Journey ",v+1," · ",b.length," recorded steps"]})]}),e.jsx(S,{tone:w.tone,children:w.label})]}),e.jsxs("div",{className:"journey-answers",children:[e.jsxs("div",{className:"journey-answer","data-kind":"entry",children:[e.jsx(oe,{name:"journey"}),e.jsx("span",{children:"Entered from"}),e.jsx("b",{children:_i(b)})]}),e.jsxs("div",{className:"journey-answer","data-kind":"selection",children:[e.jsx(oe,{name:"play"}),e.jsx("span",{children:"Selected"}),e.jsx("b",{children:p?gn(p):"Nothing selected"})]}),e.jsxs("div",{className:"journey-answer","data-kind":"outcome",children:[e.jsx(oe,{name:w.tone==="ok"?"check":"clock"}),e.jsx("span",{children:"Outcome"}),e.jsx("b",{children:w.label})]})]}),e.jsx("ol",{className:"journey-timeline",children:b.map(g=>e.jsxs("li",{children:[e.jsx("span",{className:"timeline-dot","data-action":g.action}),e.jsxs("div",{children:[e.jsx("b",{children:Ui(g)}),e.jsx("span",{children:gn(g)})]}),e.jsx("time",{children:P(g.occurredAt)})]},`${g.journeyId}:${g.sequence}`))})]},u.key)})})})]})}function vn(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function Hi(){const[s,n]=f.useState(30),{data:t,error:i,loading:a}=Q(`/admin/api/analytics?days=${s}`),l=(t==null?void 0:t.rows)??[];return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row."}),e.jsx(U,{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(D,{label:"Window",children:e.jsxs("select",{value:s,onChange:r=>n(Number(r.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"}),e.jsx("option",{value:90,children:"90 days"})]})}),children:a?e.jsx(H,{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:l.length===0?e.jsx(X,{columns:8,children:"No events in this window."}):l.map(r=>e.jsxs("tr",{children:[e.jsx("td",{children:vn(r.rowId)}),e.jsx("td",{className:"muted",children:vn(r.rowKind)}),e.jsx("td",{className:"num",children:we(r.dwellMs)}),e.jsx("td",{className:"num",children:y(r.impressions)}),e.jsx("td",{className:"num",children:y(r.focuses)}),e.jsx("td",{className:"num",children:y(r.selects)}),e.jsx("td",{className:"num",children:Je(r.selectRate)}),e.jsx("td",{className:"num",children:y(r.viewers)})]},`${r.rowId}:${r.rowKind}`))})]})})})]})}function zi(){const[s,n]=f.useState(7),{data:t,error:i,loading:a}=Q(`/admin/api/searches?days=${s}`),l=(t==null?void 0:t.terms)??[],r=(t==null?void 0:t.recent)??[],c=t==null?void 0:t.totals;return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Searches",intro:"What viewers have been looking for, and what was searched just now."}),e.jsx(U,{message:i}),e.jsx(ce,{tiles:[{label:"searches",value:y((c==null?void 0:c.searches)??0),icon:"search",tone:"info"},{label:"distinct queries",value:y((c==null?void 0:c.queries)??0),icon:"list",tone:"data"},{label:"viewers searching",value:y((c==null?void 0:c.viewers)??0),icon:"people",tone:"note"},{label:"history kept",value:`${(t==null?void 0:t.retentionDays)??30} days`,small:!0,icon:"clock"}]}),a?e.jsx(H,{}):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(D,{label:"Window",children:e.jsxs("select",{value:s,onChange:d=>n(Number(d.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:l.length===0?e.jsx(X,{columns:4,children:"Nothing searched in this window."}):l.map(d=>e.jsxs("tr",{children:[e.jsx("td",{children:d.query}),e.jsx("td",{className:"num",children:y(d.searches)}),e.jsx("td",{className:"num",children:y(d.viewers)}),e.jsx("td",{className:"muted nowrap",children:P(d.lastAt)})]},d.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(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:r.length===0?e.jsx(X,{columns:3,children:"No searches in this window."}):r.map((d,o)=>e.jsxs("tr",{children:[e.jsx("td",{className:"muted nowrap",children:P(d.occurredAt)}),e.jsx("td",{children:d.username||e.jsx(S,{tone:"warn",children:d.userId||"unknown"})}),e.jsx("td",{children:d.query})]},`${d.occurredAt}:${o}`))})]})})})]})]})}function fn(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 Ki(){const{data:s,error:n,loading:t}=Q("/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(B,{title:"Views",intro:"How often people reach Memby’s home screen. This measures app use, not playback streams."}),e.jsx(U,{message:n}),t?e.jsx(H,{}):e.jsxs(e.Fragment,{children:[e.jsx(ce,{tiles:[{label:fn((s==null?void 0:s.today.visits)??0,(s==null?void 0:s.lastWeek.visits)??0),value:y(s==null?void 0:s.today.visits),icon:"overview",tone:"data"},{label:fn((s==null?void 0:s.today.viewers)??0,(s==null?void 0:s.lastWeek.viewers)??0),value:y(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 signed-in 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(X,{columns:3,children:"No home-screen visits yet."}):i.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:y(l.visits)}),e.jsx("td",{className:"num",children:y(l.viewers)})]},l.label))})]})})}),e.jsx(T,{title:"Today by hour",intro:"Local New Zealand time. Use this to see when viewers are opening Memby.",icon:"clock",tone:"info",children:e.jsx(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(X,{columns:3,children:"No home-screen visits yet today."}):a.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:y(l.visits)}),e.jsx("td",{className:"num",children:y(l.viewers)})]},l.label))})]})})})]})]})}const Gi=s=>s.mediaType==="episode"?`${s.seriesTitle} S${String(s.seasonNumber).padStart(2,"0")}E${String(s.episodeNumber).padStart(2,"0")}`:s.title;function Zi(){var a;const s=Q("/admin/api/media-reports",{pollMs:15e3}),{busy:n,run:t}=ie(),i=(l,r)=>t(`${l.id}-${r}`,async()=>{await q.post(`/admin/api/media-reports/${l.id}/status`,{status:r}),await s.reload()});return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Media reports",intro:"Viewer-reported problems and the individual replacement searches they asked Memby to start."}),s.loading?e.jsx(H,{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:(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(l=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:Gi(l)}),e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.title})]}),e.jsxs("td",{children:[l.reason.replaceAll("_"," "),l.comment?e.jsxs(e.Fragment,{children:[e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.comment})]}):null]}),e.jsxs("td",{children:[l.reportedByUsername,e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.reportedByDevice||"Unknown device"})]}),e.jsx("td",{children:e.jsx(S,{tone:l.replacementRequested?"note":void 0,children:l.replacementRequested?l.replacementStatus||"Requested":"Not requested"})}),e.jsx("td",{children:e.jsx(S,{tone:l.status==="resolved"?"ok":l.status==="dismissed"?void 0:"warn",children:l.status})}),e.jsx("td",{className:"nowrap muted",children:P(l.createdAt)}),e.jsxs("td",{children:[e.jsx($,{size:"sm",variant:"quiet",busy:n===`${l.id}-acknowledged`,onClick:()=>void i(l,"acknowledged"),children:"Acknowledge"})," ",e.jsx($,{size:"sm",variant:"quiet",busy:n===`${l.id}-resolved`,onClick:()=>void i(l,"resolved"),children:"Resolve"})," ",e.jsx($,{size:"sm",variant:"quiet",busy:n===`${l.id}-dismissed`,onClick:()=>void i(l,"dismissed"),children:"Dismiss"})]})]},l.id))})]})}):e.jsx(ae,{children:"No media problems have been reported."})})]})}const bn={user:"",kind:"",channel:"",status:"",source:"",q:"",from:"",to:""},Yi=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:90,label:"90 days"}],Ze=100,zn={sent:"ok",delivered:"ok",failed:"bad",pending:"warn",skipped:"idle"},Ji={"in-app":"note",broadcast:"info",webhook:"data"},hs={"in-app":"In-app",broadcast:"Broadcast",webhook:"Webhook"};function Me(s){if(!s)return"—";const n=s.replace(/[-_.:]+/g," ").trim();return n.charAt(0).toUpperCase()+n.slice(1)}function ls(s,n){return(s??[]).map(t=>e.jsxs("option",{value:t.value,children:[n(t.value)," (",t.count,")"]},t.value))}function Qi(){var w,g,m,R;const[s,n]=f.useState(7),[t,i]=f.useState(bn),[a,l]=f.useState(0),[r,c]=f.useState(null),d=f.useMemo(()=>ze({...t,days:t.from?void 0:s,limit:Ze,offset:a*Ze}),[t,s,a]),o=Q(`/admin/api/notification-log${d}`),x=o.data,j=k=>{i(E=>({...E,...k})),l(0),c(null)},u=Object.values(t).some(k=>k!==""),v=x==null?void 0:x.totals,b=(x==null?void 0:x.retentionDays)??90,h=(x==null?void 0:x.entries.length)??0,p=!x||x.total===0?0:a*Ze+1;return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"Notifications",intro:"Everything Memby sent — a viewer's own news, the bar every television draws, and each outbound webhook — with what became of it. Every feature reports through one notification service, so this is the whole trail rather than whichever half a feature remembered to log.",actions:e.jsx(J,{className:"btn",to:"/admin/notification-settings",children:"TV notification settings"})}),e.jsx(U,{message:o.error}),v?e.jsx(ce,{tiles:[{label:"Sent",value:y(v.sent),icon:"send",tone:"ok"},{label:"Confirmed",value:y(v.delivered),icon:"check",tone:"ok"},{label:"Failed",value:y(v.failed),icon:"alert",tone:v.failed>0?"bad":void 0},{label:"Skipped",value:y(v.skipped),icon:"filter",tone:"idle"},{label:"People reached",value:y(v.users),icon:"people",tone:"note"},{label:"History kept",value:`${b} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Be,{value:t.from?-1:s,options:Yi.map(k=>({value:k.value,label:k.label})),onChange:k=>{n(k),j({from:"",to:""})}})}),e.jsx(D,{label:"Person",children:e.jsxs("select",{value:t.user,onChange:k=>j({user:k.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),((x==null?void 0:x.users)??[]).map(k=>e.jsx("option",{value:k.id,children:k.username||k.id},k.id))]})}),e.jsx(D,{label:"Type",children:e.jsxs("select",{value:t.kind,onChange:k=>j({kind:k.target.value}),children:[e.jsx("option",{value:"",children:"Any type"}),ls((w=x==null?void 0:x.facets)==null?void 0:w.kinds,Me)]})}),e.jsx(D,{label:"Channel",children:e.jsxs("select",{value:t.channel,onChange:k=>j({channel:k.target.value}),children:[e.jsx("option",{value:"",children:"Any channel"}),ls((g=x==null?void 0:x.facets)==null?void 0:g.channels,k=>hs[k]??Me(k))]})}),e.jsx(D,{label:"Status",children:e.jsxs("select",{value:t.status,onChange:k=>j({status:k.target.value}),children:[e.jsx("option",{value:"",children:"Any status"}),ls((m=x==null?void 0:x.facets)==null?void 0:m.statuses,Me)]})}),e.jsx(D,{label:"Source",children:e.jsxs("select",{value:t.source,onChange:k=>j({source:k.target.value}),children:[e.jsx("option",{value:"",children:"Any service"}),ls((R=x==null?void 0:x.facets)==null?void 0:R.sources,Me)]})}),e.jsx(D,{label:"From",children:e.jsx("input",{type:"date",value:t.from,onChange:k=>j({from:k.target.value})})}),e.jsx(D,{label:"To",children:e.jsx("input",{type:"date",value:t.to,onChange:k=>j({to:k.target.value})})}),e.jsx(D,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:t.q,placeholder:"Title, message, error or person",onChange:k=>j({q:k.target.value})})}),e.jsx("div",{className:"filter-actions",children:u?e.jsx($,{variant:"quiet",size:"sm",onClick:()=>{i(bn),l(0),c(null)},children:"Clear"}):null})]}),o.loading&&!x?e.jsx(H,{}):null,x&&x.days.length>1?e.jsx(T,{title:"Notifications per day",intro:"Failures and deliberate skips are counted beside the deliveries, because a quiet week and a week nothing was allowed to send look identical otherwise.",icon:"chart",tone:"info",children:e.jsx(xs,{data:x.days,labelOf:k=>k.day,valueOf:k=>k.sent+k.delivered+k.failed+k.skipped,toneOf:k=>k.failed>0?"bad":void 0,title:k=>`${k.day}: ${k.sent+k.delivered} sent, ${k.failed} failed, ${k.skipped} skipped`})}):null,x?e.jsx(T,{title:"History",intro:"Newest first. A row says who, what and whether it worked on its own; open one for the whole message and the delivery response.",icon:"send",tone:"ok",actions:e.jsx("span",{className:"filter-summary",children:x.total===0?"nothing matches":`${y(p)}–${y(p+h-1)} of ${y(x.total)}`}),footer:x.total>Ze?e.jsxs(e.Fragment,{children:[e.jsx($,{size:"sm",disabled:a===0,onClick:()=>l(a-1),children:"Newer"}),e.jsx($,{size:"sm",disabled:(a+1)*Ze>=x.total,onClick:()=>l(a+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:"Recipient"}),e.jsx("th",{children:"Type"}),e.jsx("th",{children:"Title"}),e.jsx("th",{children:"Channel"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"Status"}),e.jsx("th",{"aria-label":"Details"})]})}),e.jsx("tbody",{children:x.entries.length===0?e.jsx(X,{columns:8,children:"No notifications match these filters."}):x.entries.map(k=>e.jsxs(f.Fragment,{children:[e.jsx(Xi,{entry:k,open:r===k.id,onToggle:()=>c(r===k.id?null:k.id)}),r===k.id?e.jsx(er,{entry:k}):null]},k.id))})]})})}):null]})}function Xi({entry:s,open:n,onToggle:t}){return e.jsxs("tr",{"data-selected":n||void 0,children:[e.jsx("td",{className:"nowrap muted",children:P(s.occurredAt)}),e.jsx("td",{children:s.userId?e.jsx(J,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(s.userId)}`,children:s.username||s.userId}):s.target?e.jsx("span",{className:"muted",children:s.target}):e.jsx("span",{className:"quiet",children:"Everyone"})}),e.jsx("td",{className:"mono",children:s.kind||"—"}),e.jsx("td",{children:s.title||e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"nowrap",children:e.jsx(S,{tone:Ji[s.channel],children:hs[s.channel]??s.channel})}),e.jsx("td",{className:"muted",children:Me(s.source)}),e.jsx("td",{className:"nowrap",children:e.jsx(S,{tone:zn[s.status]??"idle",children:s.status})}),e.jsx("td",{className:"nowrap",children:e.jsx($,{size:"sm",variant:"quiet",onClick:t,icon:n?"close":"list",children:n?"Close":"Details"})})]})}function er({entry:s}){const n=Object.entries(s.metadata??{}).filter(([,t])=>t!=null&&String(t)!=="");return e.jsx("tr",{className:"detail-row",children:e.jsx("td",{colSpan:8,children:e.jsxs("section",{className:"logdrawer","aria-label":`Notification ${s.id}`,children:[e.jsxs("header",{className:"logdrawer-head",children:[e.jsxs("div",{children:[e.jsxs("p",{className:"logdrawer-place",children:[e.jsx("span",{children:hs[s.channel]??s.channel}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),Me(s.source)]}),e.jsx("b",{children:s.title||Me(s.kind)}),s.detail?e.jsx("p",{className:"logdrawer-error",children:s.detail}):null]}),e.jsx("div",{className:"logdrawer-actions",children:e.jsx(S,{tone:zn[s.status]??"idle",children:s.status})})]}),e.jsxs("div",{className:"logdrawer-grid",children:[e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Delivery"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Sent"}),e.jsx("dd",{children:P(s.occurredAt)}),s.eventAt?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Event"}),e.jsx("dd",{children:P(s.eventAt)})]}):null,e.jsx("dt",{children:"Channel"}),e.jsx("dd",{children:hs[s.channel]??s.channel}),e.jsx("dt",{children:"Type"}),e.jsx("dd",{children:s.kind||"—"}),e.jsx("dt",{children:"Service"}),e.jsx("dd",{children:Me(s.source)}),e.jsx("dt",{children:"Took"}),e.jsxs("dd",{children:[s.durationMs,"ms"]})]})]}),e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Recipient"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Person"}),e.jsx("dd",{children:s.userId?s.username||s.userId:"the whole household"}),s.target?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Destination"}),e.jsx("dd",{children:s.target})]}):null,s.itemId?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Title id"}),e.jsx("dd",{children:s.itemId})]}):null,s.sourceKey?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Source key"}),e.jsx("dd",{children:s.sourceKey})]}):null]})]}),n.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Context"}),e.jsx("dl",{children:n.map(([t,i])=>e.jsxs(f.Fragment,{children:[e.jsx("dt",{children:Me(t)}),e.jsx("dd",{children:String(i)})]},t))})]}):null]}),s.body?e.jsxs("div",{className:"logdrawer-raw",children:[e.jsx("h4",{children:"Message"}),e.jsx("p",{className:"notification-body",children:s.body})]}):null]})})})}const yn=[{value:"everywhere",label:"Everywhere",description:"Show informational banners over the home screen and active playback."},{value:"home_only",label:"Home only",description:"Show banners on the launcher, without interrupting a film or programme."},{value:"off",label:"Off",description:"Do not show informational banners on any television."}];function sr(){var v,b;const s=Q("/admin/api/gateway-settings"),n=Q("/admin/api/accounts"),{busy:t,run:i}=ie(),{wrap:a}=le(),[l,r]=f.useState(null),[c,d]=f.useState({});f.useEffect(()=>{l===null&&s.data&&r(s.data.settings.notificationDisplay||"home_only")},[l,s.data]),f.useEffect(()=>{const h=n.data;h&&d(p=>{const w={...p};for(const g of h.accounts??[])w[g.id]||(w[g.id]={...g.notifications});return w})},[n.data]);const o=()=>i("display",async()=>{if(!s.data||l===null)return;const h=await a(async()=>{const p=await q.get("/admin/api/gateway-settings");return q.post("/admin/api/gateway-settings",{...p.settings,notificationDisplay:l})},"Television notification display saved.");h&&(s.set(h),r(h.settings.notificationDisplay))}),x=h=>i(`account-${h.id}`,async()=>{var g;const p=c[h.id];if(!p)return;const w=await a(()=>q.put(`/admin/api/accounts/${encodeURIComponent(h.id)}/notifications`,p),`Notification settings saved for ${h.shortName||h.username}.`);w&&(d(m=>({...m,[h.id]:w})),n.set({accounts:(((g=n.data)==null?void 0:g.accounts)??[]).map(m=>m.id===h.id?{...m,notifications:w}:m)}))}),j=s.error||n.error,u=((v=n.data)==null?void 0:v.accounts)??[];return e.jsxs(e.Fragment,{children:[e.jsx(B,{title:"TV notifications",intro:"Choose where household notices may appear, then mute or allow them for each person.",actions:e.jsx(J,{className:"btn",to:"/admin/notifications",children:"View notification history"})}),e.jsx(U,{message:j}),s.loading||l===null?e.jsx(H,{rows:2}):e.jsx(T,{title:"Where notifications appear",intro:"This household-wide rule applies to every user and television. Maintenance and mandatory update screens still appear because they are not optional notifications.",icon:"tv",tone:"info",actions:e.jsx(S,{tone:l==="off"?void 0:"ok",children:((b=yn.find(h=>h.value===l))==null?void 0:b.label)??"Home only"}),footer:e.jsx($,{variant:"primary",icon:"check",busy:t==="display",onClick:()=>void o(),children:"Save display rule"}),children:e.jsx("div",{className:"checks columns",children:yn.map(h=>e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"radio",name:"notification-display",checked:l===h.value,onChange:()=>r(h.value)}),e.jsx("span",{className:"switch","aria-hidden":"true"}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:h.label}),e.jsx("p",{children:h.description})]})]},h.value))})}),e.jsx(T,{title:"People",intro:"A person's master switch follows them to every television. Open their user page to choose individual notification types.",icon:"people",tone:"note",children:n.loading?e.jsx(H,{}):u.length===0?e.jsx("p",{className:"empty",children:"No one has signed in to Memby yet."}):e.jsx(pe,{cols:"2",children:u.map(h=>{const p=c[h.id]??h.notifications,w=h.shortName||h.username||"Unnamed user",g=p.enabled!==h.notifications.enabled;return e.jsx(T,{title:w,intro:h.shortName?h.username:"Memby user",icon:"person",tone:"note",actions:e.jsx(S,{tone:p.enabled?"ok":void 0,children:p.enabled?"enabled":"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx($,{variant:"primary",busy:t===`account-${h.id}`,disabled:!g,onClick:()=>void x(h),children:"Save"}),e.jsx(J,{className:"btn",to:`/admin/accounts/${encodeURIComponent(h.id)}`,children:"Choose notification types"})]}),children:e.jsx(Y,{label:"All notifications",hint:"Turn off every optional notification for this person.",checked:p.enabled,onChange:m=>d(R=>({...R,[h.id]:{...p,enabled:m}}))})},h.id)})})})]})}const nr=s=>s==="detected"?"ok":s==="failed"?"bad":s==="no_match"?"warn":"info",tr=s=>s==="no_match"?"no match":s,wn=s=>({"live-playback":"Live playback","tracearr-next":"Next episode","tracearr-binge-prefetch":"Binge look-ahead","multi-user-demand":"Multiple viewers"}[s]??s)||"Unknown",kn=(s,n)=>s>0&&n>0?`S${String(s).padStart(2,"0")}E${String(n).padStart(2,"0")}`:"Episode";function ar(){var h,p,w,g;const s=Q("/admin/api/credits?limit=150",{pollMs:15e3}),{wrap:n}=le(),{busy:t,run:i}=ie(),[a,l]=f.useState(),[r,c]=f.useState(!1);f.useEffect(()=>{!r&&s.data&&l(s.data.settings)},[s.data,r]);const d=m=>{l(R=>R&&{...R,...m}),c(!0)},o=()=>{a&&i("save",async()=>{const m=await n(()=>q.put("/admin/api/credits",a),"Credits scanning settings saved.");m&&(s.set(m),l(m.settings),c(!1))})},x=((h=s.data)==null?void 0:h.history)??[],j=((p=s.data)==null?void 0:p.pending)??[],u=x.filter(m=>m.outcome==="detected").length,v=x.filter(m=>m.outcome==="no_match").length,b=x.filter(m=>m.outcome==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(B,{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(H,{}):e.jsxs(e.Fragment,{children:[(w=s.data)!=null&&w.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(ce,{tiles:[{label:"Waiting candidates",value:y((g=s.data)==null?void 0:g.queueDepth),icon:"clock",tone:j.length?"info":void 0},{label:"Detected in this history",value:y(u),icon:"check",tone:"ok"},{label:"No match",value:y(v),icon:"search",tone:v?"warn":void 0},{label:"Failed",value:y(b),icon:"alert",tone:b?"bad":void 0}]}),e.jsxs(pe,{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:!r,onClick:o,children:"Save settings"}),children:e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Candidate limit",hint:"Maximum episodes waiting in the priority queue. Stronger candidates displace weaker ones when it is full.",children:e.jsx("input",{type:"number",min:1,max:100,value:a.candidateLimit,onChange:m=>d({candidateLimit:Number(m.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:m=>d({prefetchEpisodes:Number(m.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:m=>d({maxPrefetch:Number(m.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:m=>d({retryHours:Number(m.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 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:j.length===0?e.jsx(X,{columns:6,children:"No episodes are waiting to be scanned."}):j.map(m=>e.jsxs("tr",{children:[e.jsx("td",{children:kn(m.season,m.episode)}),e.jsx("td",{children:e.jsx(S,{tone:"info",children:wn(m.reason)})}),e.jsx("td",{className:"num",children:y(m.priority)}),e.jsx("td",{className:"num",children:y(m.userCount)}),e.jsx("td",{className:"nowrap muted",title:P(m.lastViewed),children:ge(m.lastViewed)}),e.jsx("td",{className:"mono muted",children:m.itemId})]},m.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(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:x.length===0?e.jsx(X,{columns:7,children:"No credits scans have completed yet."}):x.map(m=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:P(m.finishedAt),children:ge(m.finishedAt)}),e.jsxs("td",{children:[e.jsx("b",{children:m.seriesName||m.itemName||m.itemId}),e.jsxs("span",{className:"table-sub",children:[kn(m.season,m.episode),m.itemName&&m.seriesName?` · ${m.itemName}`:""]})]}),e.jsxs("td",{children:[e.jsx(S,{tone:"info",children:wn(m.reason)}),e.jsxs("span",{className:"table-sub",children:["priority ",m.priority]})]}),e.jsxs("td",{children:[e.jsx(S,{tone:nr(m.outcome),children:tr(m.outcome)}),m.error?e.jsx("span",{className:"table-sub",children:m.error}):null]}),e.jsx("td",{className:"nowrap",children:m.markerMs>0?we(m.markerMs):"—"}),e.jsxs("td",{className:"muted",children:[m.method||"visual",m.confidence>0?` · ${Je(m.confidence)}`:"",m.frames>0?` · ${m.frames} frames`:""]}),e.jsx("td",{className:"num muted",children:we(m.durationMs)})]},m.id))})]})})})]})]})}function ir(){return e.jsx(B,{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 rr(){return e.jsx(Jn,{children:e.jsx(ft,{children:e.jsx(Nt,{children:e.jsxs(Rt,{children:[e.jsx(Qn,{children:e.jsxs(V,{path:"/admin",element:e.jsx(Tt,{}),children:[e.jsx(V,{index:!0,element:e.jsx(It,{})}),e.jsx(V,{path:"activity",element:e.jsx(Dt,{})}),e.jsx(V,{path:"accounts",element:e.jsx(qt,{})}),e.jsx(V,{path:"accounts/:userId",element:e.jsx(Ft,{})}),e.jsx(V,{path:"accounts/:userId/settings",element:e.jsx(Vt,{})}),e.jsx(V,{path:"clients",element:e.jsx(zt,{})}),e.jsx(V,{path:"logins",element:e.jsx(Gt,{})}),e.jsx(V,{path:"devices/:deviceId",element:e.jsx(Qt,{})}),e.jsx(V,{path:"library",element:e.jsx(Xt,{})}),e.jsx(V,{path:"requests",element:e.jsx(ea,{})}),e.jsx(V,{path:"recommendations",element:e.jsx(sa,{})}),e.jsx(V,{path:"inspector",element:e.jsx(aa,{})}),e.jsx(V,{path:"hero",element:e.jsx(oa,{})}),e.jsx(V,{path:"metadata-hero",element:e.jsx(da,{})}),e.jsx(V,{path:"features",element:e.jsx(ha,{})}),e.jsx(V,{path:"playback",element:e.jsx(ua,{})}),e.jsx(V,{path:"subtitles",element:e.jsx(ma,{})}),e.jsx(V,{path:"credits",element:e.jsx(ar,{})}),e.jsx(V,{path:"updates",element:e.jsx(pa,{})}),e.jsx(V,{path:"tasks",element:e.jsx(ya,{})}),e.jsx(V,{path:"tasks/:taskId",element:e.jsx(Ca,{})}),e.jsx(V,{path:"integrations",element:e.jsx(Aa,{})}),e.jsx(V,{path:"integrations/webhooks",element:e.jsx(Fa,{})}),e.jsx(V,{path:"integrations/:integrationId",element:e.jsx(Va,{})}),e.jsx(V,{path:"maintenance",element:e.jsx(Ka,{})}),e.jsx(V,{path:"runtime",element:e.jsx(Ga,{})}),e.jsx(V,{path:"settings",element:e.jsx(Qa,{})}),e.jsx(V,{path:"imports",element:e.jsx(ni,{})}),e.jsx(V,{path:"logs",element:e.jsx(Oi,{})}),e.jsx(V,{path:"journeys",element:e.jsx(Pi,{})}),e.jsx(V,{path:"journeys/:userId",element:e.jsx(Vi,{})}),e.jsx(V,{path:"views",element:e.jsx(Ki,{})}),e.jsx(V,{path:"engagement",element:e.jsx(Hi,{})}),e.jsx(V,{path:"searches",element:e.jsx(zi,{})}),e.jsx(V,{path:"media-reports",element:e.jsx(Zi,{})}),e.jsx(V,{path:"notifications",element:e.jsx(Qi,{})}),e.jsx(V,{path:"notification-settings",element:e.jsx(sr,{})}),e.jsx(V,{path:"overview",element:e.jsx(Ps,{to:"/admin",replace:!0})}),e.jsx(V,{path:"ratings",element:e.jsx(Ps,{to:"/admin/integrations/mdblist",replace:!0})}),e.jsx(V,{path:"*",element:e.jsx(ir,{})})]})}),e.jsx(Et,{})]})})})})}const Kn=document.getElementById("root");if(!Kn)throw new Error("the console has no root element to render into");Tn(Kn).render(e.jsx(f.StrictMode,{children:e.jsx(rr,{})})); diff --git a/admin-ui/dist/assets/index-CtoC2zbP.css b/admin-ui/dist/assets/index-CtoC2zbP.css new file mode 100644 index 0000000..9f1e59b --- /dev/null +++ b/admin-ui/dist/assets/index-CtoC2zbP.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}.reconnect-overlay{position:fixed;top:0;right:0;bottom:0;left:0;z-index:100;display:grid;place-items:center;padding:24px;background:#03060adb;-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px)}.reconnect-panel{display:grid;justify-items:center;width:min(440px,100%);padding:34px 32px 30px;border:1px solid var(--line);border-radius:14px;background:linear-gradient(145deg,var(--surface-lift),var(--surface));box-shadow:0 24px 80px #0006;text-align:center}.reconnect-spinner{width:30px;height:30px;margin-bottom:18px;border:3px solid var(--line);border-top-color:var(--accent-ink);border-radius:50%;animation:reconnect-spin .9s linear infinite}.reconnect-panel h1{margin:0;font-size:21px;letter-spacing:-.02em}.reconnect-panel p{max-width:35ch;margin:10px 0 18px;color:var(--muted);font-size:13px;line-height:1.55}.reconnect-panel small{color:var(--quiet);font:11.5px/1.4 var(--mono)}@keyframes reconnect-spin{to{transform:rotate(360deg)}}.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)}.link-button{border:0;padding:0;background:none;color:var(--accent-ink);font:inherit;text-decoration:underline;text-decoration-color:var(--line);text-underline-offset:3px;cursor:pointer}.is-disabled td{opacity:.62}.account-device-detail>td{padding:0 0 12px;background:var(--surface-sunken, transparent)}.device-breakdown{display:grid;gap:6px;padding:10px 12px 10px 46px;border-top:1px solid var(--line-soft)}.device-breakdown-row{display:grid;grid-template-columns:minmax(150px,1.4fr) minmax(90px,.7fr) minmax(130px,1fr) minmax(100px,1fr);gap:12px;align-items:center;padding:8px 10px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface-lift);font-size:12px}@media (max-width: 1100px){.device-breakdown-row{grid-template-columns:1fr 1fr}}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 190px minmax(240px,1fr) 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:start;padding-top:7px;padding-bottom:7px;border-bottom:1px solid rgba(255,255,255,.03);contain:strict}.logrow>*,.logrow-line{line-height:16px}.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/16px 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/16px 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:center;gap:5px;min-width:0;height:16px;overflow:hidden}.logrow-service{flex:0 0 auto;max-width:96px;overflow:hidden;color:var(--quiet);font:700 10px/16px 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;line-height:16px}.logrow-summary{display:flex;flex-direction:column;gap:2px;align-items:stretch;justify-content:flex-start;width: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;height:16px}.logrow-action{flex:0 0 auto;color:var(--muted);font:600 10.5px/16px 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-trail{flex:0 1 auto;min-width:0;max-width:38%;overflow:hidden;color:var(--quiet);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.logrow-second{height:15px;overflow:hidden;color:var(--quiet);font-size:11px;line-height:15px;text-overflow:ellipsis;white-space:nowrap}.logrow-second[data-tone=error]{color:var(--danger-ink)}.logrow-result{min-width:0;height:16px;overflow:hidden}.logrow-verdict{display:inline-block;max-width:100%;color:var(--quiet);font:500 11px/14px 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/16px 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 170px minmax(200px,1fr) 130px;gap:10px}.loghead>:last-child,.logrow-duration{display:none}.loghead,.logbody{min-width:680px}}@media (max-width: 900px){.loghead,.logrow{grid-template-columns:46px 140px minmax(180px,1fr) 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}}.detail-row>td{padding:0 0 12px;background:var(--surface-sunken, transparent)}.logdrawer-raw h4{margin:0 0 6px;color:var(--quiet);font:700 10px/1 var(--sans);letter-spacing:.08em;text-transform:uppercase}.notification-body{max-width:70ch;margin:0;color:var(--text);font:13px/1.6 var(--sans);overflow-wrap:anywhere}.subhead{display:flex;align-items:baseline;gap:8px;margin:6px 0 0;font-size:11.5px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}.subhead small{font-size:11px;font-weight:500;letter-spacing:0;text-transform:none;color:var(--quiet)}.verdict{margin:0 0 16px;padding:12px 14px;border:1px solid var(--line);border-left:3px solid var(--accent);border-radius:var(--radius-sm);background:var(--surface-lift)}.verdict[data-tone=warn]{border-left-color:var(--warn)}.verdict[data-tone=bad]{border-left-color:var(--danger)}.verdict-head{display:flex;align-items:center;gap:9px;flex-wrap:wrap;font-size:13px;color:var(--muted)}.verdict-head svg{width:17px;height:17px;color:var(--accent)}.verdict[data-tone=warn] .verdict-head svg{color:var(--warn)}.verdict[data-tone=bad] .verdict-head svg{color:var(--danger)}.verdict-head b{color:var(--text);font-size:13px}.verdict-notes{margin:11px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}.verdict-notes li{display:flex;align-items:baseline;gap:9px;font-size:12.5px;color:var(--muted)}.metadata-hero-layout{display:grid;grid-template-columns:minmax(320px,.82fr) minmax(460px,1.35fr);gap:16px;align-items:start}.metadata-hero-order{display:grid;gap:8px}.metadata-hero-order-row{display:grid;grid-template-columns:30px minmax(0,1fr) auto;align-items:center;gap:10px;padding:10px;border:1px solid var(--line-soft);border-radius:var(--radius-sm);background:var(--surface-lift);opacity:.62}.metadata-hero-order-row[data-enabled]{opacity:1}.metadata-hero-order-number{display:grid;place-items:center;width:26px;height:26px;border-radius:50%;background:var(--surface);color:var(--muted);font-size:12px;font-weight:700}.metadata-hero-order-actions{display:flex;gap:5px}.metadata-hero-order-actions button{min-width:34px;padding-inline:8px}.metadata-hero-colour{display:grid;gap:7px;margin-top:18px;padding-top:16px;border-top:1px solid var(--line-soft)}.metadata-hero-colour>b{font-size:13.5px}.metadata-hero-colour>p{margin:0;color:var(--muted);font-size:12.5px}.metadata-hero-colour .segments{width:fit-content}.metadata-hero-preview{position:relative;isolation:isolate;overflow:hidden;aspect-ratio:16 / 7.2;min-height:280px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#080b0e}.metadata-hero-preview:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;background:linear-gradient(90deg,#080b0e 0 34%,rgba(8,11,14,.9) 48%,rgba(8,11,14,.2) 76%,transparent)}.metadata-hero-preview-art{position:absolute;top:0;right:0;bottom:0;left:36%;z-index:-2;background:radial-gradient(circle at 67% 24%,rgba(211,242,255,.8) 0 2%,transparent 3%),radial-gradient(ellipse at 76% 78%,rgba(71,107,92,.8),transparent 45%),linear-gradient(145deg,#17303d,#456273 42%,#101a1f)}.metadata-hero-preview-copy{display:flex;flex-direction:column;justify-content:center;gap:10px;width:54%;min-height:100%;padding:26px 0 24px 28px}.metadata-preview-title{max-width:330px;color:#fff;font-family:Georgia,serif;font-size:clamp(24px,3vw,42px);line-height:.95;letter-spacing:.03em}.metadata-preview-ratings{display:flex;gap:8px}.metadata-preview-ratings b{padding:5px 8px;border-radius:4px;background:#ffffff1c;color:#e8edf0;font-size:11px}.metadata-preview-facts{display:flex;align-items:center;gap:7px;min-width:0;color:#bdc7ce;font-size:11px;line-height:16px;white-space:nowrap}.metadata-preview-facts span{overflow:hidden;text-overflow:ellipsis}.metadata-preview-facts b{flex:none;padding:1px 5px;border:1px solid rgba(255,255,255,.28);border-radius:3px;background:#00000038;color:#e8edf0;font-size:inherit;line-height:inherit}.metadata-preview-reason{overflow:hidden;color:var(--accent);font-size:11px;font-weight:600;line-height:16px;text-overflow:ellipsis;white-space:nowrap}.metadata-preview-progress{display:flex;align-items:center;gap:10px;color:#bdc7ce;font-size:11px;line-height:16px;white-space:nowrap}.metadata-preview-progress>span{display:block;overflow:hidden;width:112px;height:3px;border-radius:2px;background:#ffffff38}.metadata-preview-progress i{display:block;width:59%;height:100%;background:var(--accent)}.metadata-preview-progress b{color:var(--accent);font-size:inherit;line-height:inherit}.metadata-preview-progress[data-colour=white] b{color:#fff}.metadata-preview-summary{margin:0;color:#cbd2d7;font-size:12px;line-height:1.45}@media (max-width: 980px){.metadata-hero-layout{grid-template-columns:1fr}} diff --git a/admin-ui/dist/assets/index-D2yWw-VA.js b/admin-ui/dist/assets/index-D2yWw-VA.js deleted file mode 100644 index d2ede5b..0000000 --- a/admin-ui/dist/assets/index-D2yWw-VA.js +++ /dev/null @@ -1,11 +0,0 @@ -import{r as f,a as _n,u as ws,L as G,b as ks,m as vs,N as gn,O as Un,c as Be,B as Wn,R as Bn,d as W,e as Es}from"./router-D9WH5XEU.js";(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))a(i);new MutationObserver(i=>{for(const l of i)if(l.type==="childList")for(const r of l.addedNodes)r.tagName==="LINK"&&r.rel==="modulepreload"&&a(r)}).observe(document,{childList:!0,subtree:!0});function t(i){const l={};return i.integrity&&(l.integrity=i.integrity),i.referrerPolicy&&(l.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?l.credentials="include":i.crossOrigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function a(i){if(i.ep)return;i.ep=!0;const l=t(i);fetch(i.href,l)}})();var vn={exports:{}},cs={};/** - * @license React - * react-jsx-runtime.production.min.js - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */var Vn=f,Hn=Symbol.for("react.element"),zn=Symbol.for("react.fragment"),Kn=Object.prototype.hasOwnProperty,Gn=Vn.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Zn={key:!0,ref:!0,__self:!0,__source:!0};function fn(s,n,t){var a,i={},l=null,r=null;t!==void 0&&(l=""+t),n.key!==void 0&&(l=""+n.key),n.ref!==void 0&&(r=n.ref);for(a in n)Kn.call(n,a)&&!Zn.hasOwnProperty(a)&&(i[a]=n[a]);if(s&&s.defaultProps)for(a in n=s.defaultProps,n)i[a]===void 0&&(i[a]=n[a]);return{$$typeof:Hn,type:s,key:l,ref:r,props:i,_owner:Gn.current}}cs.Fragment=zn;cs.jsx=fn;cs.jsxs=fn;vn.exports=cs;var e=vn.exports,bn,As=_n;bn=As.createRoot,As.hydrateRoot;const yn={overview:"M4 13h6V4H4zm0 7h6v-4H4zm10 0h6v-9h-6zm0-16v4h6V4z",library:"M3 5h18v14H3zM7 5v14M17 5v14M3 9.5h4M3 14.5h4M17 9.5h4M17 14.5h4",people:"M15 19v-1.2a3.3 3.3 0 0 0-3.3-3.3H6.8A3.3 3.3 0 0 0 3.5 17.8V19M9.2 11a3.2 3.2 0 1 0 0-6.4 3.2 3.2 0 0 0 0 6.4ZM17 10.6a3 3 0 0 0-1.4-5.7M20.5 19v-1.2a3.3 3.3 0 0 0-2.4-3.2",person:"M18 20v-1.5a4 4 0 0 0-4-4h-4a4 4 0 0 0-4 4V20M12 10.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z",tv:"M4 5h16v10H4zM9 19h6M12 15v4M8 2.5 12 5l4-2.5",sliders:"M4 7h10M18 7h2M4 17h2m4 0h10M14 4v6M6 14v6",clock:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 7.2V12l3 1.8",pulse:"M3 12h3.5L9 19l5-14 2.5 7H21",chart:"M4 19V9m5 10V5m5 14v-7m5 7V3",chip:"M8 8h8v8H8zM4.5 4.5h15v15h-15zM9 2v2.5M15 2v2.5M9 19.5V22M15 19.5V22M2 9h2.5M2 15h2.5M19.5 9H22M19.5 15H22",database:"M12 8.2c4.4 0 8-1.2 8-2.6S16.4 3 12 3 4 4.2 4 5.6s3.6 2.6 8 2.6ZM4 5.6v12.8C4 19.8 7.6 21 12 21s8-1.2 8-2.6V5.6M4 12c0 1.4 3.6 2.6 8 2.6s8-1.2 8-2.6",download:"M12 3.5v10m0 0 4-4m-4 4-4-4M4.5 18h15",upload:"M12 16V4m0 0L8 8m4-4 4 4M5 13v6h14v-6",sync:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",search:"M10.5 17a6.5 6.5 0 1 1 0-13 6.5 6.5 0 0 1 0 13Zm4.6-1.9L20 20",star:"m12 3.2 2.6 5.4 5.9.8-4.3 4.1 1 5.9-5.2-2.8-5.2 2.8 1-5.9L3.5 9.4l5.9-.8L12 3.2Z",sparkle:"m10 3 1.5 4.3L16 8.8l-4.5 1.5L10 14.6 8.5 10.3 4 8.8l4.5-1.5L10 3ZM17.5 14l.9 2.4 2.6.9-2.6.9-.9 2.4-.9-2.4-2.6-.9 2.6-.9.9-2.4Z",bell:"M6.2 9.5a5.8 5.8 0 1 1 11.6 0c0 4.6 2.2 5.9 2.2 5.9H4s2.2-1.3 2.2-5.9M10 19.5a2 2 0 0 0 4 0",shield:"m12 3 7.5 3v5.4c0 5-3.2 8.2-7.5 9.6-4.3-1.4-7.5-4.6-7.5-9.6V6L12 3Zm-2.6 8.7 1.9 1.9 3.6-3.6",wrench:"m14.5 6.5 3-3 3 3-3 3M9 15l-5.5 5.5M13 4a5 5 0 0 0 6.5 6.5L10 20l-6-6 9.5-9.5Z",play:"M8 5.2v13.6L19 12 8 5.2ZM4 5v14",list:"M4 7h16M4 12h16M4 17h10",inbox:"M4 7h16v13H4zM8 4h8v3M8 12h8M8 16h5",send:"M21 3 10.5 13.5M21 3l-6.8 18-3.7-7.5L3 10z",history:"M3.5 12a8.5 8.5 0 1 0 2.8-6.3M3.5 4v4h4M12 7.5V12l3 1.8",check:"m5 12.5 4.5 4.5L19 7.5",alert:"M12 8.5v5m0 3.2h.01M10.3 4.4 2.7 17.5a2 2 0 0 0 1.7 3h15.2a2 2 0 0 0 1.7-3L13.7 4.4a2 2 0 0 0-3.4 0Z",power:"M12 3v9M7.5 6.2a7.5 7.5 0 1 0 9 0",key:"M14.5 3a6.5 6.5 0 1 0 3.4 12L19 14h2v-2h2V9.5l-2.5-2.5A6.5 6.5 0 0 0 14.5 3Zm-2.6 4.6a1.6 1.6 0 1 1-2.3 2.3 1.6 1.6 0 0 1 2.3-2.3Z",captions:"M4 5.5h16v13H4zM7 15h5m3 0h2M7 11h3m2 0h5",journey:"M4 6h5v5h6v7h5M7 3 4 6l3 3m10 6 3 3-3 3",plug:"M9 3v6M15 3v6M6.5 9h11v3.5a5.5 5.5 0 0 1-11 0zM12 18v3",calendar:"M4 6h16v15H4zM8 3v5M16 3v5M4 11h16",trash:"M4 7h16M9 7V4.5h6V7M6.5 7l1 13h9l1-13M10 11v5M14 11v5",plus:"M12 5v14M5 12h14",close:"M6 6l12 12M18 6 6 18",caret:"m6 9 6 6 6-6",external:"M14 4h6v6M20 4l-9 9M18 14v5.5H4.5V6H10",refresh:"M3.5 12a8.5 8.5 0 0 1 14.6-6M20.5 12a8.5 8.5 0 0 1-14.6 6M18 2.5V6h-3.5M6 21.5V18h3.5",filter:"M3.5 5.5h17l-6.5 7.5V20l-4-2v-5L3.5 5.5Z",menu:"M4 7h16M4 12h16M4 17h16",globe:"M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM3.5 9h17M3.5 15h17M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18Z",logout:"M15 17l5-5-5-5M20 12H9M12 4H5v16h7"};function ne({name:s,className:n}){const t=yn[s];return t?e.jsx("svg",{className:n??"ico",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",preserveAspectRatio:"xMidYMid meet","aria-hidden":"true",children:e.jsx("path",{d:t})}):null}function Qe({name:s,tone:n}){return yn[s]?e.jsx("span",{className:"glyph","data-tone":n,children:e.jsx(ne,{name:s})}):null}const Oe=[{id:"everyday",label:"Everyday",defaultCollapsed:!1,items:[{id:"overview",path:"/admin",label:"Overview",title:"Overview",intro:"What the gateway is doing right now.",icon:"overview"},{id:"activity",path:"/admin/activity",label:"Activity",title:"Activity",intro:"Every administrative event, newest first.",icon:"bell",badge:"notifications"},{id:"accounts",path:"/admin/accounts",label:"Users",title:"Users",intro:"Who uses Memby, and the devices they are signed in on.",icon:"people"},{id:"requests",path:"/admin/requests",label:"Media requests",title:"Media requests",intro:"Who can ask for something the library does not have.",icon:"inbox"},{id:"notifications",path:"/admin/notifications",label:"Notifications",title:"Notifications",intro:"Everything Memby sent: who it went to, over which channel, and whether it worked.",icon:"send"},{id:"notification-settings",path:"/admin/notification-settings",label:"TV notifications",title:"TV notifications",intro:"Choose where notifications appear and who receives them.",icon:"bell"},{id:"media-reports",path:"/admin/media-reports",label:"Media reports",title:"Media reports",intro:"Problems viewers reported with a film or episode.",icon:"alert"},{id:"updates",path:"/admin/updates",label:"App updates",title:"App updates",intro:"Publish an optional or a required client update.",icon:"upload"},{id:"logs",path:"/admin/logs",label:"Logs",title:"Logs",intro:"Structured gateway events as they happen.",icon:"list"}]},{id:"people",label:"Devices & access",defaultCollapsed:!0,items:[{id:"account",path:"/admin/accounts/:userId",label:"User",title:"User",intro:"Devices, recommendation setup and synced settings for one person.",icon:"person",hidden:!0},{id:"settings-history",path:"/admin/accounts/:userId/settings",label:"Settings history",title:"Settings history",intro:"Every change to one person's synced settings, and which devices took it.",icon:"sliders",hidden:!0},{id:"clients",path:"/admin/clients",label:"Devices",title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands.",icon:"tv"},{id:"logins",path:"/admin/logins",label:"Sign-ins",title:"Sign-in history",intro:"Every connection attempt: who, which television, from where, and whether it got in.",icon:"key"},{id:"device",path:"/admin/devices/:deviceId",label:"Device",title:"Device",intro:"One television: how often it connects, at what times, and from which addresses.",icon:"tv",hidden:!0}]},{id:"content",label:"Content & discovery",defaultCollapsed:!0,items:[{id:"library",path:"/admin/library",label:"Library",title:"Library",intro:"Import and inspect the catalogue Memby ranks.",icon:"library"},{id:"hero",path:"/admin/hero",label:"Home hero",title:"Home hero",intro:"Choose films or television shows for the launcher spotlight while recent releases fill the remaining places.",icon:"star"},{id:"metadata-hero",path:"/admin/metadata-hero",label:"Metadata hero",title:"Metadata hero",intro:"Order the focused-title information shown above browse rows on every television.",icon:"tv"},{id:"recommendations",path:"/admin/recommendations",label:"For You",title:"For You",intro:"The prepared pools personalised rows are drawn from.",icon:"sparkle"},{id:"inspector",path:"/admin/inspector",label:"Score inspector",title:"Score inspector",intro:"Re-run the ranker for one person and read every component.",icon:"search"}]},{id:"experience",label:"Viewing experience",defaultCollapsed:!0,items:[{id:"features",path:"/admin/features",label:"Client configuration",title:"Client configuration",intro:"Control everything the thin TV client renders, without releasing an APK.",icon:"sliders"},{id:"playback",path:"/admin/playback",label:"Playback",title:"Playback",intro:"Presentation policy sent with every playback launch.",icon:"play"},{id:"subtitles",path:"/admin/subtitles",label:"Subtitles",title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from.",icon:"captions"},{id:"credits",path:"/admin/credits",label:"Credits detection",title:"Credits detection",intro:"Control predictive scanning and review every completed credits scan.",icon:"clock"}]},{id:"operations",label:"Operations",defaultCollapsed:!0,items:[{id:"tasks",path:"/admin/tasks",label:"Scheduled tasks",title:"Scheduled tasks",intro:"What the gateway does in the background, when it last ran and whether it worked.",icon:"clock"},{id:"task",path:"/admin/tasks/:taskId",label:"Task",title:"Task",intro:"One scheduled task: its cadence, its switch and its own run history.",icon:"clock",hidden:!0},{id:"imports",path:"/admin/imports",label:"Imports",title:"Imports",intro:"Catalogue synchronisation history.",icon:"database"},{id:"runtime",path:"/admin/runtime",label:"Runtime",title:"Runtime",intro:"Goroutines, memory and the background workers inside the gateway process.",icon:"chip"},{id:"maintenance",path:"/admin/maintenance",label:"Maintenance",title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time.",icon:"wrench"},{id:"gateway-settings",path:"/admin/settings",label:"Gateway settings",title:"Gateway settings",intro:"Timezone, logging and the other server-level settings for this gateway.",icon:"sliders",hidden:!0}]},{id:"integrations",label:"Integrations",defaultCollapsed:!0,items:[{id:"integrations",path:"/admin/integrations",label:"Overview",title:"Integrations",intro:"Every external service Memby depends on: configured, working, and what it last did.",icon:"plug"},{id:"integration",path:"/admin/integrations/:integrationId",label:"Integration",title:"Integration",intro:"One external service: its switch, its settings, its jobs and its run history.",icon:"plug",hidden:!0},{id:"webhooks",path:"/admin/integrations/webhooks",label:"Event webhooks",title:"Event webhooks",intro:"Send administrative events to Discord and, in time, elsewhere.",icon:"send"}]},{id:"insights",label:"Insights",defaultCollapsed:!0,items:[{id:"views",path:"/admin/views",label:"Views",title:"App views",intro:"Home-screen visits, viewers and the times Memby is used.",icon:"overview"},{id:"searches",path:"/admin/searches",label:"Searches",title:"Searches",intro:"What viewers have been looking for, and what was searched just now.",icon:"search"},{id:"journeys",path:"/admin/journeys",label:"Journeys",title:"User journeys",intro:"How viewers move through Memby, use features and complete flows.",icon:"journey"},{id:"engagement",path:"/admin/engagement",label:"Row engagement",title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row.",icon:"chart"}]}],Yn=Oe.flatMap(s=>s.items),Jn=Oe.flatMap(s=>s.items.filter(n=>!n.path.includes(":")).map(n=>({...n,group:s.label??""})));class Is extends Error{constructor(n,t){super(n),this.status=t,this.name="ApiError"}}const Qn=5*60*1e3;let wn=Date.now();for(const s of["pointerdown","pointermove","keydown","wheel","scroll"])window.addEventListener(s,()=>{wn=Date.now()},{passive:!0});const Xn=()=>Date.now()-wn({}));throw new Is(a.error??`Request failed (${t.status})`,t.status)}if(t.status!==204)return await t.json()}function Ve(s){const n=new URLSearchParams;for(const[a,i]of Object.entries(s))i==null||i===""||i===!1||n.set(a,String(i));const t=n.toString();return t?`?${t}`:""}const q={get:s=>Xe(s),post:(s,n)=>Xe(s,{method:"POST",body:n===void 0?void 0:JSON.stringify(n)}),put:(s,n)=>Xe(s,{method:"PUT",body:n===void 0?void 0:JSON.stringify(n)}),del:s=>Xe(s,{method:"DELETE"})},st=3e4,kn=f.createContext(null);function nt({children:s}){var h;const[n,t]=f.useState(),[a,i]=f.useState(!1),[l,r]=f.useState(""),[d,c]=f.useState(""),[o,u]=f.useState(!0),j=f.useRef(0),p=f.useCallback(async()=>{const m=++j.current;try{const y=await q.get("/admin/api/status");if(m!==j.current)return;t(y),i(!0),c("")}catch(y){if(m!==j.current)return;i(!1),c(y instanceof Error?y.message:String(y))}finally{m===j.current&&(r(new Date().toISOString()),u(!1))}},[]),g=f.useCallback(async m=>{var y;await q.post("/admin/api/maintenance",{enabled:m,message:((y=n==null?void 0:n.maintenance)==null?void 0:y.message)??""}),await p()},[p,(h=n==null?void 0:n.maintenance)==null?void 0:h.message]);f.useEffect(()=>{p();let m;const y=()=>{window.clearInterval(m),m=document.hidden?void 0:window.setInterval(()=>void p(),st)},v=()=>{y(),document.hidden||p()};return y(),document.addEventListener("visibilitychange",v),()=>{window.clearInterval(m),document.removeEventListener("visibilitychange",v)}},[p]);const b=f.useMemo(()=>{var m;return{status:n,version:(n==null?void 0:n.serverVersion)??"",currentUser:((m=n==null?void 0:n.currentUser)==null?void 0:m.trim())||"Administrator",online:a,checkedAt:l,error:d,loading:o,reload:p,setMaintenance:g}},[n,a,l,d,o,p,g]);return e.jsx(kn.Provider,{value:b,children:s})}function je(){const s=f.useContext(kn);if(!s)throw new Error("useGateway used outside GatewayProvider");return s}function tt(s,n){const t=s.label.toLowerCase(),a=s.group.toLowerCase();return t.startsWith(n)?4:t.includes(n)?3:a.includes(n)?2:`${s.title} ${s.intro}`.toLowerCase().includes(n)?1:0}function it(){const s=ws(),{status:n}=je(),[t,a]=f.useState(!1),[i,l]=f.useState(""),[r,d]=f.useState(0),c=f.useRef(null),o=f.useRef(null),u=f.useMemo(()=>{const p=i.trim().toLowerCase();return[...Jn,...((n==null?void 0:n.requestUsers)??[]).map(b=>({id:`user-${b.id}`,path:`/admin/accounts/${encodeURIComponent(b.id)}`,label:b.username||"Unnamed user",title:`User: ${b.username||"Unnamed user"}`,intro:"Open this user’s devices and settings.",group:"Users",icon:"people"})),...((n==null?void 0:n.clients)??[]).map(b=>({id:`device-${b.deviceId}`,path:`/admin/devices/${encodeURIComponent(b.deviceId)}`,label:b.deviceName||"Unnamed device",title:`Device: ${b.deviceName||"Unnamed device"}`,intro:`${b.username||"Unknown user"} · ${b.version||"unknown version"}`,group:"Devices",icon:"tv"}))].map((b,h)=>({item:b,rank:p?tt(b,p):1,index:h})).filter(b=>b.rank>0).sort((b,h)=>h.rank-b.rank||b.index-h.index).map(({item:b,rank:h})=>({item:b,rank:h}))},[i,n]);f.useEffect(()=>d(0),[i]),f.useEffect(()=>{const p=g=>{var b;(b=c.current)!=null&&b.contains(g.target)||a(!1)};return document.addEventListener("pointerdown",p),()=>document.removeEventListener("pointerdown",p)},[]),f.useEffect(()=>{const p=g=>{var h,m;if(g.key!=="S"||!g.shiftKey||g.ctrlKey||g.metaKey||g.altKey)return;const b=document.activeElement;b&&(b.isContentEditable||/^(INPUT|TEXTAREA|SELECT)$/.test(b.tagName))||(g.preventDefault(),(h=o.current)==null||h.focus(),(m=o.current)==null||m.select())};return window.addEventListener("keydown",p),()=>window.removeEventListener("keydown",p)},[]);const j=p=>{var g;a(!1),l(""),(g=o.current)==null||g.blur(),s(p)};return e.jsxs("div",{className:"omni",ref:c,"data-open":t||void 0,children:[e.jsxs("div",{className:"omni-input",children:[e.jsx(ne,{name:"search"}),e.jsx("input",{ref:o,type:"search",value:i,placeholder:"Search","aria-label":"Search","aria-expanded":t,onFocus:()=>a(!0),onChange:p=>{l(p.target.value),a(!0)},onKeyDown:p=>{var g;if(p.key==="Escape")l(""),a(!1),(g=o.current)==null||g.blur();else if(p.key==="ArrowDown")p.preventDefault(),d(b=>Math.min(b+1,u.length-1));else if(p.key==="ArrowUp")p.preventDefault(),d(b=>Math.max(b-1,0));else if(p.key==="Enter"){const b=u[r];if(!b)return;p.preventDefault(),j(b.item.path)}}}),e.jsx("span",{className:"omni-key",children:"⇧S"})]}),t?e.jsx("div",{className:"omni-panel",role:"listbox",children:u.length===0?e.jsx("p",{className:"empty",children:"No pages, users or devices match that search."}):u.map((p,g)=>e.jsxs("a",{className:g===r?"omni-item on":"omni-item",href:p.item.path,role:"option","aria-selected":g===r,onPointerEnter:()=>d(g),onClick:b=>{b.preventDefault(),j(p.item.path)},children:[p.item.icon?e.jsx(ne,{name:p.item.icon}):null,e.jsxs("span",{children:[e.jsx("b",{children:p.item.label}),e.jsx("small",{children:p.item.intro})]}),e.jsx("span",{className:"omni-group",children:p.item.group})]},p.item.id))}):null]})}const w=s=>(s??0).toLocaleString(),F=s=>s?new Date(s).toLocaleString():"—";function ye(s){if(!s)return"0s";if(s<1e3)return`${Math.round(s)}ms`;const n=Math.round(s/1e3);if(n<60)return`${n}s`;const t=Math.floor(n/60);return t<60?`${t}m ${n%60}s`:`${Math.floor(t/60)}h ${t%60}m`}function _e(s){const n=Math.round(Math.max(0,s??0)/6e4);if(n<=0)return"none";if(n<60)return`${n} min`;const t=Math.floor(n/60),a=n%60;return a===0?t===1?"1 hour":`${t} hours`:`${t}h ${a}m`}function Ne(s){if(!s||s<=0)return"on request only";if(s<60)return`every ${s}s`;const n=Math.round(s/60);if(n<60)return`every ${n} min`;const t=Math.round(n/60);return t<48?t===1?"hourly":`every ${t} hours`:`every ${Math.round(t/24)} days`}function he(s){const n=["B","KB","MB","GB"];let t=Number(s??0),a=0;for(;t>=1024&&aString(s??"?").trim().split(/\s+/).slice(0,2).map(n=>n[0]??"").join("").toUpperCase(),as=s=>`${Math.round((s??0)*100)}%`;function fe(s){if(!s)return"—";const n=Date.now()-new Date(s).getTime();if(n<0)return"just now";const t=Math.floor(n/1e3);if(t<45)return"just now";const a=Math.floor(t/60);if(a<60)return`${a} min ago`;const i=Math.floor(a/60);if(i<24)return`${i}h ago`;const l=Math.floor(i/24);return l<30?`${l}d ago`:new Date(s).toLocaleDateString()}function ds(s){if(!s)return"—";const n=new Date(s).getTime()-Date.now();if(!Number.isFinite(n))return"—";if(n<=0)return"due now";const t=Math.round(n/1e3);if(t<60)return`in ${t}s`;const a=Math.round(t/60);if(a<60)return`in ${a} min`;const i=Math.round(a/60);return i<24?`in ${i}h`:`in ${Math.round(i/24)}d`}const Sn=15*60*1e3,at=3*60*60*1e3,Ye=s=>!!s&&Date.now()-new Date(s).getTime(){var y;try{const v=await q.get(`/admin/api/notifications?limit=${Ds}`);t(v.events),i(v.unread),r(v.types),j.current=Math.max(j.current,((y=v.events[0])==null?void 0:y.id)??0),u("")}catch(v){u(v instanceof Error?v.message:String(v))}},[]),g=f.useCallback(y=>{j.current=Math.max(j.current,y.id),t(v=>v.some(x=>x.id===y.id)?v:[y,...v].sort((x,$)=>$.id-x.id).slice(0,Ds)),y.readAt||i(v=>v+1),r(v=>v.some(x=>x.type===y.type)?v.map(x=>x.type===y.type?{...x,count:x.count+1}:x):[...v,{type:y.type,count:1}])},[]);f.useEffect(()=>{p()},[p]),f.useEffect(()=>{let y=null,v,x=!1;return(()=>{x||(y=new EventSource(`/admin/api/notifications/stream?after=${j.current}`),y.addEventListener("open",()=>{c(!0),window.clearInterval(v),v=void 0}),y.addEventListener("admin",k=>{try{g(JSON.parse(k.data))}catch{}}),y.addEventListener("error",()=>{c(!1),v===void 0&&(v=window.setInterval(()=>void p(),rt))}))})(),()=>{x=!0,y==null||y.close(),window.clearInterval(v)}},[g,p]);const b=f.useCallback(async y=>{const v=y.filter(x=>x>0);if(v.length!==0){t(x=>x.map($=>v.includes($.id)&&!$.readAt?{...$,readAt:new Date().toISOString()}:$));try{const x=await q.post("/admin/api/notifications/read",{ids:v});i(x.unread)}catch{p()}}},[p]),h=f.useCallback(async()=>{i(0),t(y=>y.map(v=>v.readAt?v:{...v,readAt:new Date().toISOString()}));try{const y=await q.post("/admin/api/notifications/read",{all:!0});i(y.unread)}catch{p()}},[p]),m=f.useMemo(()=>({events:n,unread:a,types:l,connected:d,error:o,markRead:b,markAllRead:h,reload:p}),[n,a,l,d,o,b,h,p]);return e.jsx(Cn.Provider,{value:m,children:s})}function Ss(){const s=f.useContext(Cn);if(!s)throw new Error("useNotifications used outside NotificationProvider");return s}function Mn(s){return s.severity==="error"?"bad":s.severity==="warning"?"warn":"info"}function Tn(s){return s.startsWith("auth.")?"key":s.startsWith("device.")?"tv":s.startsWith("admin.")?"shield":s.startsWith("task.")?"clock":s.startsWith("integration.")?"plug":s.startsWith("library.")?"library":s.startsWith("emby.")?"globe":s.startsWith("server.")?"power":"bell"}function qs(s){return{"auth.login":"Signed in","auth.login_failed":"Sign-in refused","auth.logout":"Signed out","device.registered":"New device","device.removed":"Device removed","device.renamed":"Device renamed","admin.sign_in":"Admin sign-in","server.started":"Server started","server.maintenance":"Maintenance","task.completed":"Task finished","task.failed":"Task failed","integration.failed":"Integration failed","integration.test":"Integration test","library.sync":"Library sync","emby.unreachable":"Emby unreachable","emby.recovered":"Emby recovered"}[s]??s.replace(/[._]/g," ")}const Fs=99;function ot(){const{events:s,unread:n,connected:t,markRead:a,markAllRead:i}=Ss(),[l,r]=f.useState(!1),d=f.useRef(null);return f.useEffect(()=>{const c=u=>{var j;(j=d.current)!=null&&j.contains(u.target)||r(!1)},o=u=>{u.key==="Escape"&&r(!1)};return document.addEventListener("pointerdown",c),document.addEventListener("keydown",o),()=>{document.removeEventListener("pointerdown",c),document.removeEventListener("keydown",o)}},[]),f.useEffect(()=>{if(!l)return;const c=s.filter(o=>!o.readAt).map(o=>o.id);c.length>0&&a(c)},[l]),e.jsxs("div",{className:"bell",ref:d,"data-open":l||void 0,children:[e.jsxs("button",{type:"button",className:"bell-button","aria-label":n>0?`Activity, ${n} unread`:"Activity","aria-expanded":l,onClick:()=>r(c=>!c),children:[e.jsx(ne,{name:"bell"}),n>0?e.jsx("span",{className:"bell-badge",children:n>Fs?`${Fs}+`:n}):null]}),l?e.jsxs("div",{className:"bell-panel",children:[e.jsxs("div",{className:"bell-head",children:[e.jsx("b",{children:"Activity"}),e.jsxs("div",{className:"row tight",children:[t?null:e.jsx("span",{className:"tag","data-tone":"warn",children:"reconnecting"}),n>0?e.jsx("button",{type:"button","data-variant":"quiet","data-size":"sm",onClick:()=>void i(),children:"Mark all read"}):null]})]}),e.jsx("div",{className:"bell-list",children:s.length===0?e.jsx("p",{className:"empty",children:"Nothing has happened yet."}):s.slice(0,20).map(c=>{const o=e.jsxs(e.Fragment,{children:[e.jsx(Qe,{name:Tn(c.type),tone:Mn(c)}),e.jsxs("span",{className:"bell-body",children:[e.jsx("b",{children:c.title||c.type}),c.summary?e.jsx("p",{children:c.summary}):null,e.jsx("time",{dateTime:c.occurredAt,children:fe(c.occurredAt)})]})]});return c.link?e.jsx(G,{className:"bell-item","data-unread":!c.readAt||void 0,to:c.link,onClick:()=>r(!1),children:o},c.id):e.jsx("div",{className:"bell-item","data-unread":!c.readAt||void 0,children:o},c.id)})}),e.jsx("div",{className:"bell-foot",children:e.jsx(G,{to:"/admin/activity",onClick:()=>r(!1),children:"All activity"})})]}):null]})}function Y(s,n={}){const{pollMs:t,enabled:a=!0}=n,[i,l]=f.useState(),[r,d]=f.useState(""),[c,o]=f.useState(a),[u,j]=f.useState(!1),p=f.useRef(0),g=f.useRef(!1),b=f.useCallback(async()=>{if(!a)return;const h=++p.current;g.current&&j(!0);try{const m=await q.get(s);if(h!==p.current)return;l(m),d(""),g.current=!0}catch(m){if(h!==p.current)return;d(m instanceof Error?m.message:String(m))}finally{h===p.current&&(o(!1),j(!1))}},[s,a]);return f.useEffect(()=>(g.current=!1,o(!0),b(),()=>{p.current+=1}),[b]),f.useEffect(()=>{if(!t||!a)return;let h;const m=()=>{window.clearInterval(h),h=document.hidden?void 0:window.setInterval(()=>void b(),t)},y=()=>{m(),document.hidden||b()};return m(),document.addEventListener("visibilitychange",y),()=>{window.clearInterval(h),document.removeEventListener("visibilitychange",y)}},[t,a,b]),{data:i,error:r,loading:c,refreshing:u,reload:b,set:l}}function se(){const[s,n]=f.useState(null),t=f.useRef(!0);f.useEffect(()=>()=>{t.current=!1},[]);const a=f.useCallback(async(i,l)=>{n(i);try{return await l(),!0}finally{t.current&&n(null)}},[]);return{busy:s,run:a}}const Os="Memby";function ct(s){f.useEffect(()=>{const n=s.trim();document.title=n?`${n} | ${Os}`:Os},[s])}function U({title:s,intro:n,actions:t,crumbs:a,icon:i}){var d;const l=ks(),r=i??((d=Yn.find(c=>vs({path:c.path,end:!0},l.pathname)))==null?void 0:d.icon);return ct(s),e.jsxs("header",{className:"page-head",children:[a?e.jsx("nav",{className:"crumbs",children:a}):null,e.jsxs("div",{className:"page-head-row",children:[e.jsxs("div",{className:"page-head-title",children:[r?e.jsx("span",{className:"page-head-icon","aria-hidden":"true",children:e.jsx(ne,{name:r})}):null,e.jsxs("div",{className:"page-head-text",children:[e.jsx("h1",{children:s}),n?e.jsx("p",{children:n}):null]})]}),t?e.jsx("div",{className:"page-head-actions",children:t}):null]})]})}function M({title:s,intro:n,icon:t,tone:a,actions:i,footer:l,children:r}){return e.jsxs("section",{className:"card",children:[s?e.jsxs("div",{className:"card-head",children:[t?e.jsx(Qe,{name:t,tone:a}):null,e.jsxs("div",{className:"card-head-text",children:[e.jsx("h2",{children:s}),n?e.jsx("p",{children:n}):null]}),i?e.jsx("div",{className:"card-head-actions",children:i}):null]}):null,r,l?e.jsx("div",{className:"card-foot",children:l}):null]})}function ue({cols:s,children:n}){return e.jsx("div",{className:"grid","data-cols":s,children:n})}function ae({tiles:s}){return e.jsx("div",{className:"tiles",children:s.map(n=>e.jsxs("div",{className:"tile",children:[n.icon?e.jsx(Qe,{name:n.icon,tone:n.tone}):null,e.jsx("b",{className:n.small?"small":void 0,children:n.value}),e.jsx("span",{children:n.label})]},n.label))})}function N({children:s,tone:n}){return e.jsx("span",{className:"tag","data-tone":n,children:s})}function oe({children:s,tone:n}){return e.jsx("span",{className:"chip","data-tone":n,children:s})}function ee({children:s}){return e.jsx("p",{className:"empty",children:s})}function J({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 xe({children:s,tone:n}){return e.jsx("p",{className:"note","data-tone":n,children:s})}function T({children:s,onClick:n,variant:t,size:a,disabled:i,busy:l,icon:r,type:d="button",title:c}){return e.jsxs("button",{type:d,className:"","data-variant":t,"data-size":a,disabled:i||l,onClick:n,title:c,children:[l?e.jsx("span",{className:"spinner"}):r?e.jsx(ne,{name:r}):null,s]})}function D({label:s,hint:n,children:t,grow:a}){return e.jsxs("label",{className:a?"field grow":"field",children:[e.jsx("span",{children:s}),t,n?e.jsx("small",{children:n}):null]})}function Z({label:s,hint:n,checked:t,onChange:a,disabled:i}){return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:t,disabled:i,onChange:l=>a(l.target.checked)}),e.jsx("span",{className:"switch"}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:s}),n?e.jsx("p",{children:n}):null]})]})}function Ue({value:s,options:n,onChange:t}){return e.jsx("div",{className:"segments",role:"group",children:n.map(a=>e.jsx("button",{type:"button","aria-pressed":a.value===s,onClick:()=>t(a.value),children:a.label},String(a.value)))})}function K({children:s}){return e.jsx("div",{className:"table-wrap",children:s})}function hs({data:s,labelOf:n,valueOf:t,toneOf:a,title:i}){if(s.length===0)return e.jsx(ee,{children:"Nothing in this window."});const l=s.map(d=>t(d)),r=Math.max(1,...l);return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"bars",children:s.map((d,c)=>{const o=t(d);return e.jsx("div",{className:"bar","data-tone":a==null?void 0:a(d),"data-empty":o===0||void 0,style:{height:`${Math.max(2,o/r*100)}%`},title:i?i(d):`${n(d,c)}: ${o}`},c)})}),e.jsxs("div",{className:"bars-axis",children:[e.jsx("span",{children:n(s[0],0)}),e.jsx("span",{children:n(s[s.length-1],s.length-1)})]})]})}function $n({value:s,total:n,tone:t}){const a=n>0?Math.min(1,s/n):0;return e.jsx("div",{className:"meter","data-tone":t,children:e.jsx("div",{style:{width:`${a*100}%`}})})}function P({message:s,onDismiss:n}){return s?e.jsxs("div",{className:"banner",role:"alert",children:[e.jsx(ne,{name:"alert"}),e.jsx("span",{children:s}),n?e.jsx("button",{type:"button",onClick:n,"aria-label":"Dismiss",children:e.jsx(ne,{name:"close"})}):null]}):null}function B({rows:s=3}){return e.jsxs("div",{className:"loading-page","aria-busy":"true","aria-label":"Loading",children:[e.jsxs("div",{className:"loading-heading",children:[e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"})]}),e.jsx("div",{className:"loading-tiles",children:Array.from({length:4},(n,t)=>e.jsx("span",{className:"skeleton"},t))}),Array.from({length:s},(n,t)=>e.jsxs("section",{className:"loading-card",children:[e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"}),e.jsx("span",{className:"skeleton"})]},t))]})}function ke({title:s,body:n,confirmLabel:t="Confirm",destructive:a,busy:i,onConfirm:l,onCancel:r}){const d=f.useId(),c=f.useRef(null);return f.useEffect(()=>{var u;(u=c.current)==null||u.focus();const o=j=>{j.key==="Escape"&&r()};return document.addEventListener("keydown",o),()=>document.removeEventListener("keydown",o)},[r]),e.jsx("div",{className:"scrim",onPointerDown:o=>o.target===o.currentTarget&&r(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true","aria-labelledby":d,ref:c,tabIndex:-1,children:[e.jsx("h2",{id:d,children:s}),e.jsx("p",{children:n}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{onClick:r,variant:"quiet",children:"Cancel"}),e.jsx(T,{onClick:l,variant:a?"danger":"primary",busy:i,children:t})]})]})})}function We({rows:s}){return e.jsx("div",{className:"kv",children:s.map(n=>e.jsxs("div",{className:"kv-row",children:[e.jsx("span",{children:n.label}),e.jsx("span",{children:n.value})]},n.label))})}function xs({children:s,aside:n}){return e.jsxs("h4",{className:"subhead",children:[e.jsx("span",{children:s}),n?e.jsx("small",{children:n}):null]})}function Cs({tiles:s}){return e.jsx("div",{className:"tiles plain",children:s.map(n=>e.jsxs("div",{className:"tile",children:[e.jsx("b",{className:n.small?"small":void 0,children:n.value}),e.jsx("span",{children:n.label})]},n.label))})}function dt({open:s,onNavigate:n}){const{unread:t}=Ss(),a=ks(),[i,l]=f.useState(()=>{try{return JSON.parse(localStorage.getItem("memby-admin-nav")??"{}")}catch{return{}}}),r=c=>{try{localStorage.setItem("memby-admin-nav",JSON.stringify(c))}catch{}};f.useEffect(()=>{const c=Oe.find(o=>o.items.some(u=>vs({path:u.path,end:!0},a.pathname)));c&&l(o=>{const u={...o};return Oe.forEach(j=>{j.collapsible!==!1&&(u[j.id]=j.id!==c.id)}),r(u),u})},[a.pathname]);const d=(c,o=!1)=>{l(u=>{const j={...u},p=!(u[c]??o);return Oe.forEach(g=>{g.collapsible!==!1&&(j[g.id]=g.id===c?p:!0)}),r(j),j})};return e.jsx("nav",{className:"rail",id:"rail","data-open":s||void 0,"aria-label":"Console sections",children:Oe.map(c=>{const o=c.items.filter(g=>!g.hidden);if(o.length===0)return null;const u=o.some(g=>vs({path:g.path,end:!0},a.pathname)),j=c.collapsible!==!1,p=u||!j||!(i[c.id]??c.defaultCollapsed??!1);return e.jsxs("div",{className:"rail-group",children:[c.label&&j?e.jsxs("button",{type:"button",className:"rail-head","aria-expanded":p,onClick:()=>d(c.id,c.defaultCollapsed),children:[c.label,e.jsx(ne,{name:"caret",className:"ico caret"})]}):c.label?e.jsx("div",{className:"rail-head rail-head-static",children:c.label}):null,p?o.map(g=>e.jsx(gn,{to:g.path,end:g.path==="/admin",onClick:n,className:({isActive:b})=>b?"on":"","aria-current":void 0,children:({isActive:b})=>e.jsxs("span",{style:{display:"contents"},ref:h=>{const m=h==null?void 0:h.parentElement;m&&(b?m.setAttribute("aria-current","page"):m.removeAttribute("aria-current"))},children:[g.icon?e.jsx(ne,{name:g.icon}):null,g.label,g.badge==="notifications"&&t>0?e.jsx("span",{className:"rail-badge",children:t>99?"99+":t}):null]})},g.id)):null]},c.id)})})}function ht(){var k,R,I;const{version:s,currentUser:n,online:t,loading:a,status:i,setMaintenance:l}=je(),[r,d]=f.useState(!1),[c,o]=f.useState(!1),[u,j]=f.useState(!1),[p,g]=f.useState(!1),b=ks(),h=!!((k=i==null?void 0:i.maintenance)!=null&&k.enabled),m=!!((R=i==null?void 0:i.quietTime)!=null&&R.active),y=f.useRef(null),v=((I=Array.from(n.trim())[0])==null?void 0:I.toLocaleUpperCase("en-NZ"))||"A",x=i&&t&&!h&&!m?"ok":i||!a?"bad":"checking",$=async()=>{if(!(u||!i)){j(!0);try{await l(!h)}finally{j(!1)}}};return f.useEffect(()=>d(!1),[b.pathname]),f.useEffect(()=>{if(!c)return;const A=X=>{var le;(le=y.current)!=null&&le.contains(X.target)||o(!1)},L=X=>{X.key==="Escape"&&o(!1)};return document.addEventListener("mousedown",A),window.addEventListener("keydown",L),()=>{document.removeEventListener("mousedown",A),window.removeEventListener("keydown",L)}},[c]),f.useEffect(()=>{if(!r)return;const A=document.body.style.overflow;document.body.style.overflow="hidden";const L=X=>{X.key==="Escape"&&d(!1)};return window.addEventListener("keydown",L),()=>{document.body.style.overflow=A,window.removeEventListener("keydown",L)}},[r]),e.jsxs(e.Fragment,{children:[e.jsx("a",{className:"skip",href:"#main",children:"Skip to content"}),e.jsxs("header",{className:"topbar",children:[e.jsxs("a",{className:"topbar-brand",href:"/admin",children:[e.jsx("span",{className:"brand-mark","aria-hidden":"true",children:"M"}),e.jsx("span",{className:"brand-word",children:"Memby Gateway"})]}),e.jsx("button",{type:"button",className:"rail-toggle","aria-label":"Sections","aria-expanded":r,"aria-controls":"rail",onClick:()=>d(A=>!A),children:e.jsx(ne,{name:"menu"})}),e.jsx("div",{className:"topbar-spacer"}),e.jsxs("div",{className:"topbar-tools",children:[e.jsx(it,{}),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:!i||!t||u||m,title:i?m?"Memby quiet time is active":h?"Bring Memby back online":t?"Take Memby offline":"Memby is not responding":"Checking Memby status","aria-label":m?"Memby quiet time is active":h?"Memby is offline. Bring it online":i&&t?"Memby is online. Take it offline":a?"Checking Memby status":"Memby is not responding",onClick:()=>h?void $():g(!0),children:e.jsx("span",{className:"dot","aria-hidden":"true"})}),e.jsx(ot,{}),e.jsxs("div",{className:"account-menu","data-open":c||void 0,ref:y,children:[e.jsxs("button",{type:"button",className:"account-trigger","aria-haspopup":"menu","aria-expanded":c,"aria-label":`Signed in as ${n}`,onClick:()=>o(A=>!A),children:[e.jsx("span",{className:"account-avatar","aria-hidden":"true",children:v}),e.jsx("span",{className:"account-name",children:n}),e.jsx(ne,{name:"caret",className:"ico account-caret"})]}),c?e.jsxs("div",{className:"account-panel",role:"menu",children:[e.jsxs("div",{className:"account-identity",children:[e.jsx("span",{className:"account-avatar account-avatar-large","aria-hidden":"true",children:v}),e.jsxs("span",{children:[e.jsx("small",{children:"Signed in as"}),e.jsx("b",{children:n})]})]}),e.jsxs(gn,{to:"/admin/settings",role:"menuitem",onClick:()=>o(!1),children:[e.jsx(ne,{name:"sliders"}),"Gateway settings"]}),e.jsx("form",{method:"post",action:"/admin/logout",children:e.jsxs("button",{type:"submit",role:"menuitem",children:[e.jsx(ne,{name:"logout"}),"Log out"]})})]}):null]})]})]}),r?e.jsx("button",{type:"button",className:"rail-scrim","aria-label":"Close sections",onClick:()=>d(!1)}):null,e.jsx(dt,{open:r,onNavigate:()=>d(!1)}),e.jsx("main",{className:"page",id:"main",children:e.jsx(Un,{})}),p?e.jsx(ke,{title:"Take Memby offline?",body:"Every television will stop working immediately. Viewers will see the maintenance message configured on the Maintenance page, while this console remains available.",confirmLabel:"Go offline",destructive:!0,busy:u,onConfirm:()=>{g(!1),$()},onCancel:()=>g(!1)}):null]})}const Rn=f.createContext(null),ut=5e3;function mt({children:s}){const[n,t]=f.useState([]),a=f.useRef(1),i=f.useCallback(c=>{t(o=>o.filter(u=>u.id!==c))},[]),l=f.useCallback((c,o="ok")=>{const u=a.current++;t(j=>[...j,{id:u,message:c,tone:o}]),window.setTimeout(()=>i(u),ut)},[i]),r=f.useCallback(async(c,o)=>{try{const u=await c();return o&&l(o,"ok"),u}catch(u){l(u instanceof Error?u.message:String(u),"bad");return}},[l]),d=f.useMemo(()=>({show:l,wrap:r}),[l,r]);return e.jsxs(Rn.Provider,{value:d,children:[s,e.jsx("div",{className:"toasts",role:"status","aria-live":"polite",children:n.map(c=>e.jsxs("div",{className:"toast","data-tone":c.tone,children:[e.jsx(ne,{name:c.tone==="bad"?"alert":"check"}),e.jsx("span",{children:c.message}),e.jsx("button",{type:"button",onClick:()=>i(c.id),"aria-label":"Dismiss",children:e.jsx(ne,{name:"close"})})]},c.id))})]})}function te(){const s=f.useContext(Rn);if(!s)throw new Error("useToast used outside ToastProvider");return s}const Ps={ok:"ok",watch:"warn",bad:"bad"},pt={ok:"Healthy",watch:"Worth watching",bad:"Needs attention"};function En({health:s,compact:n}){const t=s.notes??[];return e.jsxs("div",{className:"verdict","data-tone":Ps[s.level]??"ok",children:[e.jsxs("div",{className:"verdict-head",children:[e.jsx(ne,{name:s.level==="ok"?"check":"alert"}),e.jsx("b",{children:pt[s.level]??"Unknown"}),e.jsx("span",{children:s.summary})]}),n||t.length===0?null:e.jsx("ul",{className:"verdict-notes",children:t.map((a,i)=>e.jsxs("li",{children:[e.jsx(N,{tone:Ps[a.level]??"note",children:a.area}),e.jsx("span",{children:a.message})]},`${a.area}-${i}`))})]})}function rs(s,n){switch(s.direction){case"rising":return`rising ${n(Math.abs(s.perHour))} an hour`;case"falling":return`falling ${n(Math.abs(s.perHour))} an hour`;case"steady":return"steady";default:return"gathering history"}}function An(s){if(!s||s<60)return`${Math.max(0,Math.round(s))}s`;const n=Math.floor(s/60);if(n<60)return`${n} min`;const t=Math.floor(n/60);return t<48?`${t}h ${n%60}m`:`${Math.floor(t/24)} days`}function xt(){var y,v,x,$,k,R;const{status:s,error:n,loading:t}=je(),a=Y("/admin/api/runtime",{pollMs:3e4}),i=Y("/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(P,{message:n}),e.jsx(B,{})]});const l=s.features??{features:[],revision:0,safeMode:!1},r=l.features??[],d=s.clients??[],c=d.filter(I=>Ye(I.lastSeen)).length,o=s.updatePolicy??{},u=!!o.minimumVersion&&o.minimumVersion===o.latestVersion,j=s.playbackPolicy,p=s.mdblist,g=s.forYou,b=(s.runs??[]).slice(0,5),h=a.data,m=h?h.memory.memoryLimit>0&&h.memory.memoryLimitI.enabled).length} / ${r.length}`,icon:"sliders",tone:"ok"},{label:"last import",value:F(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsxs(ue,{cols:"2",children:[e.jsx(M,{title:"What televisions are being told",intro:"The answers the gateway is giving every set right now.",icon:"tv",tone:"info",children:e.jsx(We,{rows:[{label:"Availability",value:(k=s.maintenance)!=null&&k.enabled?e.jsx(N,{tone:"bad",children:"offline for maintenance"}):(R=s.quietTime)!=null&&R.active?e.jsx(N,{tone:"warn",children:"quiet time active"}):e.jsx(N,{tone:"ok",children:"online"})},{label:"Feature control plane",value:l.safeMode?e.jsx(N,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(N,{tone:"ok",children:["revision r",w(l.revision)]})},{label:"App update prompt",value:o.enabled?e.jsxs(N,{tone:u?"warn":"ok",children:[u?"required · ":"optional · ",o.latestVersion]}):e.jsx(N,{children:"off"})},{label:"Catalogue import",value:s.syncRunning?e.jsx(N,{tone:"warn",children:"running"}):e.jsxs(N,{children:["every ",s.syncEvery]})},{label:"Playback preroll",value:(j==null?void 0:j.prerollEnabled)===!1?e.jsx(N,{children:"off"}):e.jsxs(N,{tone:"ok",children:[((j==null?void 0:j.prerollDurationMs)??6500)/1e3,"s"]})}]})}),e.jsx(M,{title:"Services",intro:"The services this gateway leans on, and whether they answered.",icon:"wrench",tone:"note",actions:e.jsx(G,{to:"/admin/integrations",children:"Integrations"}),children:e.jsx(We,{rows:[{label:"Movies (Radarr)",value:s.radarrReady?e.jsx(N,{tone:"ok",children:"ready"}):e.jsx(N,{children:"not configured"})},{label:"Series (Sonarr)",value:s.sonarrReady?e.jsx(N,{tone:"ok",children:"ready"}):e.jsx(N,{children:"not configured"})},{label:"MDBList ratings",value:p!=null&&p.enabled?e.jsxs(N,{tone:"ok",children:[w(p.cachedTitles)," titles stored"]}):e.jsx(N,{children:p!=null&&p.apiKeyConfigured?"off · key saved":"off · no key"})},{label:"For You pools",value:s.forYouRunning?e.jsx(N,{tone:"warn",children:"rebuilding"}):e.jsxs(N,{children:[w((g==null?void 0:g.candidates)??0)," ranked candidates"]})},{label:"Recommendation profiles",value:e.jsx("span",{className:"mono",children:w((g==null?void 0:g.profiles)??0)})}]})})]}),e.jsxs(ue,{cols:"2",children:[e.jsx(M,{title:"Latest imports",intro:"The last few catalogue synchronisations.",icon:"sync",tone:"data",actions:e.jsx(G,{to:"/admin/imports",children:"All imports"}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Started"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"num",children:"Written"})]})}),e.jsx("tbody",{children:b.length===0?e.jsx(J,{columns:4,children:"No imports have run yet."}):b.map(I=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(I.startedAt)}),e.jsx("td",{children:I.kind}),e.jsx("td",{children:e.jsx(N,{tone:I.status==="success"?"ok":I.status==="running"?"warn":"bad",children:I.status})}),e.jsx("td",{className:"num",children:w(I.itemsUpserted)})]},I.id||I.startedAt))})]})})}),e.jsx(M,{title:"Process",intro:"The container the gateway is served from.",icon:"chip",tone:"info",actions:e.jsx(G,{to:"/admin/runtime",children:e.jsx(T,{size:"sm",variant:"quiet",icon:"external",children:"Runtime details"})}),children:h?e.jsxs(e.Fragment,{children:[e.jsx(En,{health:h.health,compact:!0}),e.jsx(Cs,{tiles:[{label:`heap used · ${rs(h.heapTrend,he)}`,value:he(h.memory.heapInuse)},{label:"of the process limit",value:m?`${(h.memory.heapShare*100).toFixed(1)}%`:"no limit"},{label:`goroutines · ${rs(h.goroutineTrend,I=>w(Math.round(I)))}`,value:w(h.goroutines)},{label:"running for",value:An(h.uptimeSeconds)}]}),e.jsxs("p",{className:"hint",children:["Heap in use is what the gateway is holding;"," ",he(h.memory.sys)," is reserved from the operating system on its behalf, which is always larger and is not a leak.",m?` The limit is ${he(h.memory.memoryLimit)}${h.memory.configuredLimit?" (GOMEMLIMIT)":""}.`:" No memory limit is set."]})]}):e.jsx("p",{className:"hint",children:"Reading process statistics…"})})]})]})}const jt=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"}];function gt(){const{unread:s,connected:n,markAllRead:t,reload:a}=Ss(),[i,l]=f.useState(7),[r,d]=f.useState(""),[c,o]=f.useState(""),[u,j]=f.useState(!1),[p,g]=f.useState(0),b=100,h=f.useMemo(()=>`/admin/api/notifications${Ve({days:i,type:r,severity:c,unread:u,limit:b,offset:p*b})}`,[i,r,c,u,p]),{data:m,error:y,loading:v,reload:x}=Y(h),$=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(T,{onClick:()=>void $(),icon:"check",children:"Mark all read"}):void 0}),e.jsx(P,{message:y}),e.jsx(ae,{tiles:[{label:"Events in window",value:w((m==null?void 0:m.total)??0),icon:"bell",tone:"info"},{label:"Unread",value:w(s),icon:"alert",tone:s>0?"warn":void 0},{label:"Kinds seen",value:w((m==null?void 0:m.types.length)??0),icon:"list",tone:"note"},{label:"Live feed",value:n?"connected":"reconnecting",small:!0,icon:"pulse",tone:n?"ok":"warn"}]}),e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Ue,{value:i,options:jt.map(k=>({value:k.value,label:k.label})),onChange:k=>{l(k),g(0)}})}),e.jsx(D,{label:"Kind",children:e.jsxs("select",{value:r,onChange:k=>{d(k.target.value),g(0)},children:[e.jsx("option",{value:"",children:"Everything"}),((m==null?void 0:m.types)??[]).map(k=>e.jsxs("option",{value:k.type,children:[qs(k.type)," (",k.count,")"]},k.type))]})}),e.jsx(D,{label:"Severity",children:e.jsxs("select",{value:c,onChange:k=>{o(k.target.value),g(0)},children:[e.jsx("option",{value:"",children:"Any"}),e.jsx("option",{value:"info",children:"Information"}),e.jsx("option",{value:"warning",children:"Warning"}),e.jsx("option",{value:"error",children:"Error"})]})}),e.jsx(D,{label:"Read state",children:e.jsxs("select",{value:u?"unread":"",onChange:k=>{j(k.target.value==="unread"),g(0)},children:[e.jsx("option",{value:"",children:"All"}),e.jsx("option",{value:"unread",children:"Unread only"})]})}),e.jsx("div",{className:"filter-actions",children:e.jsx(T,{variant:"quiet",size:"sm",icon:"refresh",onClick:()=>{x(),a()},children:"Refresh"})})]}),v?e.jsx(B,{}):e.jsx(M,{title:"Events",icon:"bell",tone:"info",footer:((m==null?void 0:m.total)??0)>b?e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",disabled:p===0,onClick:()=>g(p-1),children:"Newer"}),e.jsx(T,{size:"sm",disabled:(p+1)*b>=((m==null?void 0:m.total)??0),onClick:()=>g(p+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"What happened"}),e.jsx("th",{children:"Who"}),e.jsx("th",{children:"What"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:((m==null?void 0:m.events.length)??0)===0?e.jsx(J,{columns:6,children:"Nothing has happened in this window."}):m==null?void 0:m.events.map(k=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(k.occurredAt),children:fe(k.occurredAt)}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(Qe,{name:Tn(k.type),tone:Mn(k)}),qs(k.type)]})}),e.jsxs("td",{children:[e.jsx("b",{children:k.title}),k.summary?e.jsx("div",{className:"muted",children:k.summary}):null]}),e.jsx("td",{className:"muted nowrap",children:k.actor||"—"}),e.jsx("td",{className:"muted nowrap",children:k.target||"—"}),e.jsxs("td",{className:"nowrap",children:[k.readAt?null:e.jsx(N,{tone:"ok",children:"new"}),k.link?e.jsx(G,{className:"table-row-link",to:k.link,children:"Open"}):null]})]},k.id))})]})})})]})}function _s(s){const n=s?new Date(s).getTime():0;return Number.isFinite(n)?n:0}function Us(){return e.jsx("span",{className:"muted",title:"No Tracearr sessions matched to this person",children:"—"})}function vt(){const{data:s,error:n,loading:t}=Y("/admin/api/accounts",{pollMs:6e4}),a=(s==null?void 0:s.accounts)??[],i=a.flatMap(u=>u.devices??[]),l=a.filter(u=>{var j;return(j=u.recommendations)==null?void 0:j.completed}).length,r=a.filter(u=>{var j,p;return((j=u.recommendations)==null?void 0:j.prompted)&&!((p=u.recommendations)!=null&&p.completed)}).length,d=a.filter(u=>{var j;return(j=u.watchTime)==null?void 0:j.matched}),c=d.reduce((u,j)=>{var p;return u+(((p=j.watchTime)==null?void 0:p.weekMs)??0)},0),o=[...a].sort((u,j)=>_s(j.lastSeen)-_s(u.lastSeen));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Users",intro:"Who uses Memby, and which devices they are signed in to."}),e.jsx(P,{message:n}),t?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Memby users",value:w(a.length),icon:"people",tone:"note"},{label:"signed-in devices",value:w(i.length),icon:"tv",tone:"info"},{label:"active in the last 15 mins",value:w(i.filter(u=>Ye(u.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"recommendation setups completed",value:w(l),icon:"check",tone:"ok"},{label:"setup prompts queued",value:w(r),icon:"sparkle",tone:"note"},...d.length?[{label:"watch time this week",value:_e(c),icon:"pulse",tone:"data"}]:[]]}),e.jsx(M,{title:"Users",icon:"people",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"User"}),e.jsx("th",{children:"Short name"}),e.jsx("th",{className:"num",children:"Devices"}),e.jsx("th",{className:"num",children:"This week"}),e.jsx("th",{className:"num",children:"This month"}),e.jsx("th",{children:"Recommendations"}),e.jsx("th",{children:"Last seen"})]})}),e.jsx("tbody",{children:o.length===0?e.jsx(J,{columns:7,children:"No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here."}):o.map(u=>{var m,y;const j=u.devices??[],p=j.filter(v=>Ye(v.lastSeen)).length,g=(m=u.recommendations)!=null&&m.completed?{label:"personalised",tone:"ok"}:(y=u.recommendations)!=null&&y.prompted?{label:"prompt queued",tone:"warn"}:{label:"not invited",tone:void 0},b=Ns(u.lastSeen),h=u.watchTime;return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":b.tone,title:b.label}),e.jsx("span",{className:"avatar",children:u.initials||Nn(u.username)}),e.jsx(G,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(u.id)}`,children:u.username||"Unnamed user"})]})}),e.jsx("td",{children:u.shortName||e.jsx("span",{className:"muted",title:"Memby greets them by their account name",children:"account name"})}),e.jsxs("td",{className:"num",children:[w(j.length),p?e.jsxs("span",{className:"table-sub",children:[w(p)," active now"]}):null]}),e.jsx("td",{className:"num",children:h!=null&&h.matched?_e(h.weekMs):e.jsx(Us,{})}),e.jsx("td",{className:"num muted",children:h!=null&&h.matched?_e(h.monthMs):e.jsx(Us,{})}),e.jsx("td",{children:e.jsx(N,{tone:g.tone,children:g.label})}),e.jsx("td",{className:"nowrap muted",title:F(u.lastSeen),children:fe(u.lastSeen)})]},u.id)})})]})})})]})]})}function js(s){const n=String(s??"").replace("#","");return n.length!==8?`#${n}`:`#${n.slice(2)}${n.slice(0,2)}`}function ft(){var z,ce,we,$e,Re,ge;const{userId:s=""}=Be(),n=ws(),{wrap:t}=te(),{busy:a,run:i}=se(),l=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:r,error:d,loading:c,reload:o}=Y("/admin/api/accounts",{pollMs:3e4}),[u,j]=f.useState(null),[p,g]=f.useState(null),[b,h]=f.useState(null),[m,y]=f.useState(null),[v,x]=f.useState(null),[$,k]=f.useState(null),R=Y(`${l}/viewers`),I=((r==null?void 0:r.accounts)??[]).find(E=>E.id===s),A=(((z=R.data)==null?void 0:z.viewers)??[]).filter(E=>E.kind!=="main"),L=!!((ce=R.data)!=null&&ce.enabled)&&A.length<(((we=R.data)==null?void 0:we.maxShadowViewers)??0),X=(r==null?void 0:r.catalogue)??[],le=(r==null?void 0:r.themes)??[];f.useEffect(()=>{var E;u===null&&I&&j({...((E=I.settings)==null?void 0:E.preferences)??{}})},[I,u]),f.useEffect(()=>{b===null&&I&&h({...I.notifications})},[I,b]),f.useEffect(()=>{if(p!==null||!I)return;const E=I.themes??[];g(E.length===0?le.map(_=>_.id):E)},[I,p,le]);const me=f.useMemo(()=>{const E=[];for(const _ of X){let de=E.find(pe=>pe.name===_.area);de||E.push(de={name:_.area,definitions:[]}),de.definitions.push(_)}return E},[X]),ie=(E,_,de,pe)=>i(E,async()=>{const Ie=await t(_,de);y(null),Ie!==void 0&&(pe==null||pe()),await o()});if(c)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(G,{to:"/admin/accounts",children:"← All users"})}),e.jsx(B,{})]});if(!I)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User",crumbs:e.jsx(G,{to:"/admin/accounts",children:"← All users"})}),e.jsx(P,{message:d}),e.jsx(M,{children:e.jsx(ee,{children:"This user is no longer signed in to Memby."})})]});const H=I.devices??[],re=H.filter(E=>Ye(E.lastSeen)).length,S=I.settings??{},O=I.recommendations??{};return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:I.username||"Unnamed user",intro:`Memby user · ${I.shortName?`greeted as ${I.shortName} · `:""}${w(H.length)} device${H.length===1?"":"s"} · last seen ${F(I.lastSeen)}`,crumbs:e.jsx(G,{to:"/admin/accounts",children:"← All users"}),actions:e.jsxs(e.Fragment,{children:[e.jsx("span",{className:"avatar",children:I.initials||Nn(I.username)}),re?e.jsxs(N,{tone:"ok",children:[re," active now"]}):e.jsx(N,{children:"idle"}),e.jsx(oe,{children:I.id})]})}),e.jsx(P,{message:d}),e.jsxs(ue,{cols:"wide",children:[e.jsx(M,{title:"Devices",intro:"Every build a set has been seen running is listed under it. Signing one out revokes its Memby session, drops that history and removes it from Emby's own device list. Its Emby account is not changed.",icon:"tv",tone:"info",children:H.length===0?e.jsx(ee,{children:"No devices are signed in to this user."}):e.jsx("div",{className:"list",children:H.map(E=>{const _=Ns(E.lastSeen);return e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsxs("b",{children:[e.jsx("span",{className:"dot-state","data-tone":_.tone,title:_.label})," ",e.jsx(G,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(E.id)}`,children:E.name||"Memby TV"})]}),e.jsxs("p",{children:[E.version?`Memby ${E.version}`:"Legacy Memby client"," · ",_.label," · last seen ",F(E.lastSeen)," · signed in ",F(E.signedInAt)]}),(E.versions??[]).length>0?e.jsx("div",{className:"chips",children:(E.versions??[]).map(de=>e.jsxs(oe,{tone:de.version===E.version?"ok":void 0,children:[de.version,de.version===E.version?" · now":""]},de.version))}):null]}),e.jsxs("div",{className:"list-actions",children:[e.jsx(T,{size:"sm",disabled:!E.id,onClick:()=>x({id:E.id,name:E.name}),children:"Rename"}),e.jsx(T,{size:"sm",variant:"danger",disabled:!E.id,onClick:()=>y({kind:"remove-device",deviceId:E.id,name:E.name}),children:"Sign out"})]})]},E.id||E.name)})})}),e.jsxs(M,{title:"Viewers",intro:"Several people under one Emby account, each with their own Continue Watching, watched history and favourites. Only the account itself is synced with Emby; a shadow viewer's watching is kept by Memby and never reported.",icon:"people",tone:"note",actions:R.data&&!R.data.enabled?e.jsx(N,{tone:"warn",children:"switched off"}):e.jsxs(N,{tone:"ok",children:[w(A.length)," beside the account"]}),footer:L?e.jsx(T,{variant:"primary",onClick:()=>k({id:null,name:""}),children:"Add viewer"}):e.jsx(T,{disabled:!0,children:R.data?`${w(R.data.maxShadowViewers)} is the limit`:"Add viewer"}),children:[R.data&&!R.data.enabled?e.jsx(P,{message:"Viewers are switched off for this server, so every television watches as the account. Nothing here is deleted — turn the feature on under Features to bring these people back."}):null,R.loading?e.jsx(B,{}):((($e=R.data)==null?void 0:$e.viewers)??[]).length===0?e.jsx(ee,{children:"Nobody is set up yet, so this account has one viewer: itself."}):e.jsx("div",{className:"list",children:(((Re=R.data)==null?void 0:Re.viewers)??[]).map(E=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:E.name}),e.jsxs("p",{children:[E.kind==="main"?"The account itself — named by Emby, and the only one whose watching Emby hears about":"Watches privately; nothing reaches Emby",E.createdAt&&E.kind!=="main"?` · added ${F(E.createdAt)}`:""]})]}),e.jsx("div",{className:"list-actions",children:E.kind==="main"?e.jsx(N,{tone:"info",children:"synced with Emby"}):e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",onClick:()=>k({id:E.id,name:E.name}),children:"Rename"}),e.jsx(T,{size:"sm",variant:"danger",onClick:()=>y({kind:"remove-viewer",viewer:E}),children:"Remove"})]})})]},E.id))})]}),e.jsxs(M,{title:"Recommendation setup",intro:"The prompt appears the next time this person opens Memby on any of their televisions.",icon:"sparkle",tone:"note",footer:O.completed?e.jsx(T,{busy:a==="reset-rec",onClick:()=>y({kind:"reset-recommendations"}),children:"Clear stored choices"}):O.prompted?e.jsx(T,{onClick:()=>y({kind:"cancel-prompt"}),children:"Cancel prompt"}):e.jsx(T,{variant:"primary",busy:a==="prompt",onClick:()=>void ie("prompt",()=>q.put(`${l}/recommendations/prompt`),"Setup prompt queued."),children:"Send setup prompt"}),children:[e.jsx("div",{className:"row tight",children:O.completed?e.jsx(N,{tone:"ok",children:"completed"}):O.prompted?e.jsx(N,{tone:"warn",children:"prompt queued"}):e.jsx(N,{children:"not invited"})}),e.jsx(bt,{prompt:O})]})]}),(ge=I.watchTime)!=null&&ge.matched?e.jsx(M,{title:"Watch time",intro:"From Tracearr, for this person across every client — not only Memby. The week runs from Monday and the month from the first, both in the server's own timezone.",icon:"pulse",tone:"data",actions:I.watchTime.tracearrUsername?e.jsx(oe,{children:I.watchTime.tracearrUsername}):null,children:e.jsx(ae,{tiles:[{label:`this week · ${w(I.watchTime.weekSessions)} session${I.watchTime.weekSessions===1?"":"s"}`,value:_e(I.watchTime.weekMs),icon:"pulse",tone:"data"},{label:`this month · ${w(I.watchTime.monthSessions)} session${I.watchTime.monthSessions===1?"":"s"}`,value:_e(I.watchTime.monthMs),icon:"calendar",tone:"info"},{label:"since Tracearr started recording",value:_e(I.watchTime.totalMs),icon:"clock",tone:"note"},{label:"last watched",value:F(I.watchTime.lastWatchedAt),icon:"history",tone:void 0,small:!0}]})}):null,e.jsx(M,{title:"Notifications",intro:"Choose what this person sees across every television. Changes apply through the gateway within a few seconds and do not require an app release.",icon:"bell",tone:"note",actions:b!=null&&b.enabled?e.jsx(N,{tone:"ok",children:"enabled"}):e.jsx(N,{children:"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:a==="notifications",onClick:()=>void ie("notifications",()=>q.put(`${l}/notifications`,b),"Notification settings saved.",()=>h(null)),children:"Save notifications"}),e.jsx(T,{onClick:()=>{h(null),o()},children:"Discard changes"})]}),children:b?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:b.enabled,onChange:E=>h(_=>_&&{..._,enabled:E})}),e.jsx(Z,{label:"My Shows return dates",hint:"Remind this person when a followed show is about to return.",checked:b.showReturnAlerts,disabled:!b.enabled,onChange:E=>h(_=>_&&{..._,showReturnAlerts:E})}),e.jsx(Z,{label:"Sonarr television alerts",hint:"New episodes, additions and cancellation news supplied by Sonarr.",checked:b.sonarrAlerts,disabled:!b.enabled,onChange:E=>h(_=>_&&{..._,sonarrAlerts:E})}),e.jsx(Z,{label:"Radarr film alerts",hint:"Notify this person when Radarr imports a new film.",checked:b.radarrAlerts,disabled:!b.enabled,onChange:E=>h(_=>_&&{..._,radarrAlerts:E})}),e.jsx(Z,{label:"Optional app updates",hint:"Offer new app versions to this person. Mandatory compatibility updates are always enforced.",checked:b.updateAlerts,disabled:!b.enabled,onChange:E=>h(_=>_&&{..._,updateAlerts:E})}),e.jsx(Z,{label:"Library activity",hint:"Show alerts after the Memby library catalogue is refreshed.",checked:b.libraryAlerts,disabled:!b.enabled,onChange:E=>h(_=>_&&{..._,libraryAlerts:E})}),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:b.watchTimeDigest,disabled:!b.enabled,onChange:E=>h(_=>_&&{..._,watchTimeDigest:E})}),e.jsx(Z,{label:"Service status",hint:"Memby deployment and Emby outage or recovery notices. Maintenance mode itself still applies.",checked:b.systemAlerts,disabled:!b.enabled,onChange:E=>h(_=>_&&{..._,systemAlerts:E})})]}):null}),e.jsx(M,{title:"Settings",intro:"These live on the server and follow the person, so a change here reaches every television they use — usually within a few seconds, and on the next launch for a set that is switched off.",icon:"sliders",tone:"ok",actions:S.saved?e.jsxs(N,{tone:S.source==="admin"?"warn":"ok",children:["r",w(S.revision)," · ",S.source||"device"," · ",F(S.updatedAt)]}):e.jsx(N,{children:"defaults · never synced"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:a==="push",onClick:()=>void ie("push",()=>q.put(`${l}/preferences`,{preferences:u??{}}),"Pushed to their televisions.",()=>j(null)),children:"Push to their televisions"}),e.jsx(T,{onClick:()=>{j(null),o()},children:"Discard changes"}),e.jsx(T,{onClick:()=>y({kind:"reset-preferences"}),children:"Restore defaults"}),e.jsx(G,{className:"crumb",to:`/admin/accounts/${encodeURIComponent(s)}/settings`,children:"History and rollback →"})]}),children:me.map(E=>e.jsxs("div",{className:"group",children:[e.jsx("p",{className:"group-label",children:E.name}),E.definitions.map(_=>e.jsx(yt,{definition:_,value:u==null?void 0:u[_.key],onChange:de=>j(pe=>({...pe??{},[_.key]:de}))},_.key))]},E.name))}),e.jsx(M,{title:"Colour schemes",intro:"Which palettes this person may choose between in Settings → Appearance. Tick everything to leave them unrestricted. Their current choice is an ordinary setting above; withdrawing it here puts them back on Midnight.",icon:"sparkle",tone:"note",actions:(I.themes??[]).length===0?e.jsx(N,{children:"all schemes"}):e.jsxs(N,{tone:"note",children:[w((I.themes??[]).length)," of ",w(le.length)]}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:a==="themes",onClick:()=>(p??[]).length===0?y({kind:"no-themes"}):void ie("themes",()=>q.put(`${l}/themes`,{themes:p??[]}),"Colour schemes saved.",()=>g(null)),children:"Save colour schemes"}),e.jsx(T,{onClick:()=>g(le.map(E=>E.id)),children:"Allow all"}),e.jsxs("span",{className:"hint",children:["Seasonal themes are not listed. They apply to every television in the house for their dates and nobody can decline one — the only switch is ",e.jsx("em",{children:"Seasonal themes"})," on the features page."]})]}),children:e.jsx("div",{className:"checks columns",children:le.map(E=>{var _,de,pe;return e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"checkbox",checked:(p??[]).includes(E.id),onChange:Ie=>g(He=>Ie.target.checked?[...He??[],E.id]:(He??[]).filter(C=>C!==E.id))}),e.jsx("span",{className:"switch"}),e.jsx("span",{className:"swatch",style:{"--swatch-surface":js((_=E.palette)==null?void 0:_.surface),"--swatch-accent":js((de=E.palette)==null?void 0:de.accent),"--swatch-hairline":js((pe=E.palette)==null?void 0:pe.hairline)},children:e.jsx("i",{})}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:E.name}),e.jsx("p",{children:E.description})]})]},E.id)})})}),e.jsx(M,{title:"Remove Memby access",intro:"Signs every one of this person's Memby devices out. Their Emby account, viewing history and library permissions are untouched.",icon:"alert",tone:"bad",children:e.jsx(T,{variant:"danger",onClick:()=>y({kind:"remove-account"}),children:"Remove Memby access"})}),v?e.jsx(wt,{initial:v.name,busy:a==="rename",onCancel:()=>x(null),onConfirm:E=>void ie("rename",()=>q.put(`${l}/devices/${encodeURIComponent(v.id)}`,{deviceName:E}),"Device renamed.",()=>x(null))}):null,$?e.jsx(kt,{initial:$.name,renaming:$.id!==null,busy:a==="viewer-name",onCancel:()=>k(null),onConfirm:E=>void ie("viewer-name",()=>$.id?q.put(`${l}/viewers/${encodeURIComponent($.id)}`,{name:E}):q.post(`${l}/viewers`,{name:E}),$.id?"Viewer renamed.":"Viewer added.",()=>{k(null),R.reload()})}):null,m?e.jsx(Nt,{pending:m,busy:a,username:I.username,onCancel:()=>y(null),onConfirm:()=>{switch(m.kind){case"remove-device":return void ie("remove-device",()=>q.del(`${l}/devices/${encodeURIComponent(m.deviceId)}`),"Device signed out.");case"remove-viewer":return void ie("remove-viewer",()=>q.del(`${l}/viewers/${encodeURIComponent(m.viewer.id)}`),"Viewer removed.",()=>void R.reload());case"remove-account":return void ie("remove-account",()=>q.del(`${l}/sessions`),"Memby access removed.",()=>n("/admin/accounts"));case"reset-recommendations":case"cancel-prompt":return void ie("reset-rec",()=>q.del(`${l}/recommendations`),"Recommendation choices cleared.");case"reset-preferences":return void ie("reset-prefs",()=>q.del(`${l}/preferences`),"Defaults restored.",()=>j(null));case"no-themes":return void ie("themes",()=>q.put(`${l}/themes`,{themes:[]}),"Colour schemes saved.",()=>g(null))}}}):null]})}function bt({prompt:s}){const n=s.ratings??[],t=[["Genres",s.genres],["Studios",s.studios],["Actors",s.actors],["Actresses",s.actresses],["Directors",s.directors],["Types",s.contentTypes]],a=[...n.map(i=>e.jsxs(oe,{tone:"warn",children:[i.title," · ",w(i.rating)," ★"]},`r:${i.title}`)),...t.flatMap(([i,l])=>(l??[]).map(r=>e.jsxs(oe,{children:[i,": ",r]},`${i}:${r}`)))];return a.length===0?e.jsx(ee,{children:"No recommendation selections have been saved."}):e.jsx("div",{className:"chips",children:a})}function yt({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 i=s.unit??"",l=s.kind==="number"?(s.numbers??[]).map(r=>({value:String(r),label:r===0?"No limit":i?`${r} ${i}`:String(r)})):s.options??[];return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("select",{value:String(n??""),onChange:r=>t(s.kind==="number"?Number(r.target.value):r.target.value),children:l.map(r=>e.jsx("option",{value:r.value,children:r.label},r.value))})})}if(s.kind==="multi"){const i=Array.isArray(n)?n:[],l=[...i,...(s.options??[]).map(r=>r.value).filter(r=>!i.includes(r))];return e.jsxs("div",{className:"field",children:[e.jsx("span",{children:s.name}),e.jsx("small",{children:s.description}),e.jsx("div",{className:"checks",children:l.map(r=>{const d=(s.options??[]).find(c=>c.value===r);return d?e.jsx(Z,{label:d.label,checked:i.includes(r),onChange:c=>t(c?[...i,r]:i.filter(o=>o!==r))},r):null})})]})}if(s.kind==="text")return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("input",{type:"text",value:String(n??""),maxLength:s.maxLength,placeholder:s.placeholder,onChange:i=>t(s.uppercase?i.target.value.toLocaleUpperCase("en-NZ"):i.target.value)})});const a=Array.isArray(n)?n:[];return e.jsx(D,{label:s.name,hint:s.description,children:e.jsx("textarea",{spellCheck:!1,placeholder:"One row id per line",value:a.join(` -`),onChange:i=>t(i.target.value.split(` -`).map(l=>l.trim()).filter(Boolean))})})}function wt({initial:s,busy:n,onConfirm:t,onCancel:a}){const[i,l]=f.useState(s||"Memby TV");return e.jsx("div",{className:"scrim",onPointerDown:r=>r.target===r.currentTarget&&a(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:"Name this device"}),e.jsx("p",{children:"The name a viewer sees in Settings → Devices, and what the console calls it."}),e.jsx(D,{label:"Device name",children:e.jsx("input",{type:"text",value:i,autoFocus:!0,maxLength:80,onChange:r=>l(r.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{variant:"quiet",onClick:a,children:"Cancel"}),e.jsx(T,{variant:"primary",busy:n,disabled:!i.trim(),onClick:()=>t(i.trim()),children:"Rename"})]})]})})}function kt({initial:s,renaming:n,busy:t,onConfirm:a,onCancel:i}){const[l,r]=f.useState(s);return e.jsx("div",{className:"scrim",onPointerDown:d=>d.target===d.currentTarget&&i(),children:e.jsxs("div",{className:"dialog",role:"dialog","aria-modal":"true",children:[e.jsx("h2",{children:n?"Rename this viewer":"Add a viewer"}),e.jsx("p",{children:"The name shown on the television’s “Who’s watching?” screen. Everything they watch is kept by Memby and never reported to Emby."}),e.jsx(D,{label:"Name",children:e.jsx("input",{type:"text",value:l,autoFocus:!0,maxLength:40,onChange:d=>r(d.target.value)})}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{variant:"quiet",onClick:i,children:"Cancel"}),e.jsx(T,{variant:"primary",busy:t,disabled:!l.trim(),onClick:()=>a(l.trim()),children:n?"Rename":"Add viewer"})]})]})})}function Nt({pending:s,busy:n,username:t,onConfirm:a,onCancel:i}){const r={"remove-device":{title:"Sign this device out of Memby?",body:"Its Emby account will not be changed. The set can sign in again at any time.",label:"Sign out",destructive:!0},"remove-account":{title:`Remove Memby access for ${t||"this user"}?`,body:"Every Memby device will be signed out. Their Emby account, viewing history and library permissions are untouched.",label:"Remove access",destructive:!0},"reset-recommendations":{title:"Clear this person's stored recommendation choices?",body:"Viewing history remains intact; only the explicit setup answers are removed.",label:"Clear",destructive:!0},"cancel-prompt":{title:"Cancel this person's queued recommendation prompt?",body:"They will not be invited to set up recommendations on their next launch.",label:"Cancel prompt",destructive:!1},"reset-preferences":{title:"Restore the Memby defaults for this person?",body:"Their televisions will pick the change up the next time they check in.",label:"Restore defaults",destructive:!0},"remove-viewer":{title:"Remove this viewer?",body:"What they were part-way through, what they had watched and their favourites are deleted, on every television in the house. The Emby account is untouched.",label:"Remove viewer",destructive:!0},"no-themes":{title:"Allow this person no colour schemes?",body:"They will be left on Midnight with nothing to choose between.",label:"Save anyway",destructive:!0}}[s.kind];return e.jsx(ke,{title:r.title,body:r.body,confirmLabel:r.label,destructive:r.destructive,busy:!!n,onConfirm:a,onCancel:i})}function St(s,n){if(s.kind==="toggle")return n?"On":"Off";if(s.kind==="choice"){const a=(s.options??[]).find(i=>i.value===n);return a?a.label:String(n??"")}if(s.kind==="number")return Number(n)===0&&s.unit?"No limit":s.unit?`${n} ${s.unit}`:String(n??"");const t=Array.isArray(n)?n:[];return t.length===0?"None":t.map(a=>{var i;return((i=(s.options??[]).find(l=>l.value===a))==null?void 0:i.label)??a}).join(", ")}function Ct(){const{userId:s=""}=Be(),{wrap:n}=te(),{busy:t,run:a}=se(),i=`/admin/api/accounts/${encodeURIComponent(s)}`,{data:l,error:r,loading:d,reload:c}=Y(`${i}/preferences/history`,{pollMs:3e4}),[o,u]=f.useState(new Set),[j,p]=f.useState(null),g=(l==null?void 0:l.username)||"this account",b=(l==null?void 0:l.devices)??[],h=(l==null?void 0:l.revisions)??[],m=(l==null?void 0:l.catalogue)??[],y=x=>u($=>{const k=new Set($);return k.has(x)?k.delete(x):k.add(x),k}),v=x=>a("restore",async()=>{await n(()=>q.post(`${i}/preferences/revisions/${x}/restore`),`Restored r${x}.`),p(null),await c()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Settings history",intro:`Every change to ${g}'s synced settings, and which of their televisions has taken it.`,crumbs:e.jsxs(G,{to:`/admin/accounts/${encodeURIComponent(s)}`,children:["← ",g]})}),e.jsx(P,{message:r}),d?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Where each device has got to",intro:"A set takes a change by fetching it, which it does within a few seconds of being told — so anything still behind is switched off, mid-film, or cannot reach the gateway.",icon:"tv",tone:"info",actions:l!=null&&l.saved?e.jsxs(N,{tone:l.currentSource==="admin"?"warn":"ok",children:["now on r",w(l.currentRevision)," · ",l.currentSource||"device"]}):e.jsx(N,{children:"defaults · never synced"}),children:b.length===0?e.jsx(ee,{children:"No television has been signed in to this account."}):e.jsx("div",{className:"list",children:b.map(x=>{const $=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":$})," ",x.name||"Memby TV"," ",x.signedOut?e.jsx(N,{children:"signed out"}):null]}),e.jsxs("p",{children:[x.never?"Has not fetched these settings yet":`Holding r${w(x.revision)} · taken ${F(x.ackedAt)}`,x.clientVersion?` · Memby ${x.clientVersion}`:"",x.signedOut?"":` · last seen ${F(x.lastSeen)}`]})]}),e.jsx("div",{className:"list-actions",children:x.never?e.jsx(N,{children:"never taken one"}):x.behind?e.jsxs(N,{tone:"bad",children:[w(x.behind)," behind"]}):e.jsx(N,{tone:"ok",children:"up to date"})})]},x.deviceId||x.name)})})}),e.jsx(M,{title:"Change history",intro:"Restoring puts an earlier version back as a new change, so the televisions notice it and the version it replaced stays here to return to.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{className:"num",children:"Rev"}),e.jsx("th",{children:"Changed by"}),e.jsx("th",{children:"What changed"}),e.jsx("th",{className:"num",children:"Taken by"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:h.length===0?e.jsx(J,{columns:6,children:"Nothing has been changed on this account yet."}):h.flatMap(x=>{const $=o.has(x.revision),k=x.acks??[],R=x.changes??[],I=[e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(x.createdAt)}),e.jsxs("td",{className:"num nowrap",children:["r",w(x.revision)," ",x.current?e.jsx(N,{tone:"ok",children:"current"}):null]}),e.jsxs("td",{className:"nowrap",children:[e.jsx(N,{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"}):R.length===0?e.jsx("span",{className:"muted",children:"No visible change"}):e.jsx("div",{className:"chips",children:R.map((A,L)=>e.jsxs(oe,{children:[A.name,": ",A.before," → ",A.after]},`${A.name}:${L}`))})}),e.jsx("td",{className:"num",children:k.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{title:k.map(A=>A.deviceName||A.deviceId).join(", "),children:w(k.length)})}),e.jsx("td",{className:"num nowrap",children:e.jsxs("span",{className:"list-actions",children:[e.jsx(T,{size:"sm",onClick:()=>y(x.revision),children:$?"Hide":"Show"}),x.current?null:e.jsx(T,{size:"sm",onClick:()=>p(x.revision),children:"Restore"})]})})]},x.revision)];return $&&I.push(e.jsx("tr",{children:e.jsx("td",{colSpan:6,className:"muted",children:e.jsx("div",{className:"chips",children:m.map(A=>{var L;return e.jsxs(oe,{children:[A.name,":"," ",St(A,(L=x.preferences)==null?void 0:L[A.key])]},A.key)})})})},`${x.revision}:detail`)),I})})]})})})]}),j!==null?e.jsx(ke,{title:`Restore revision ${j}?`,body:"It goes out as a new change, so every one of their televisions will pick it up — and the current version stays in this history to return to.",confirmLabel:"Restore",busy:t==="restore",onConfirm:()=>void v(j),onCancel:()=>p(null)}):null]})}function Mt({client:s}){const n=s.versions??[];return n.length===0?e.jsx("span",{className:"muted",children:"—"}):e.jsx("span",{className:"versions",children:n.map(t=>e.jsx(oe,{tone:t.version===s.version?"ok":void 0,children:t.version},t.version))})}function Tt(){const{status:s,error:n,loading:t}=je(),a=(s==null?void 0:s.clients)??[],i=a.filter(r=>(r.capabilities??[]).includes("server_features_v1")),l=new Set(a.map(r=>r.version).filter(Boolean));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Devices",intro:"Which sets have reported in, what they are running and what their build understands."}),e.jsx(P,{message:n}),t?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"devices known",value:w(a.length),icon:"tv",tone:"info"},{label:"active in the last quarter hour",value:w(a.filter(r=>Ye(r.lastSeen)).length),icon:"pulse",tone:"ok"},{label:"reporting their capabilities",value:w(i.length),icon:"sliders",tone:"ok"},{label:"app builds in service",value:w(l.size),icon:"download",tone:"note"}]}),e.jsx(M,{title:"Devices",intro:"Every request carries what that build understands. A feature is only presented to a device that declares its contract, which is what lets an older set keep working while a new one gets the new behaviour. Status is whether the set is reporting that list at all; a build old enough to say nothing is served the fallback.",icon:"tv",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Device"}),e.jsx("th",{children:"Person"}),e.jsx("th",{children:"App"}),e.jsx("th",{children:"Builds seen"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Last seen"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(J,{columns:6,children:"No devices have signed in yet."}):a.map(r=>{const d=(r.capabilities??[]).includes("server_features_v1"),c=Ns(r.lastSeen);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":c.tone,title:c.label}),e.jsx(G,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||"Memby TV"})]})}),e.jsx("td",{className:"muted",children:r.username}),e.jsx("td",{className:"mono",children:r.version||"legacy"}),e.jsx("td",{children:e.jsx(Mt,{client:r})}),e.jsx("td",{children:e.jsx(N,{tone:d?"ok":"warn",children:d?"reported":"missing"})}),e.jsx("td",{className:"nowrap muted",children:F(r.lastSeen)})]},`${r.deviceId}:${r.username}`)})})]})})})]})]})}const Ws={user:"",q:"",ip:"",outcome:"",from:"",to:""},$t=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Rt(){var x,$,k,R,I,A;const[s,n]=f.useState("log"),[t,a]=f.useState(7),[i,l]=f.useState(Ws),[r,d]=f.useState(0),c=100,o=f.useMemo(()=>Ve({...i,days:i.from?void 0:t||void 0,limit:c,offset:r*c}),[i,t,r]),u=Y(`/admin/api/logins${o}`,{enabled:s==="log"}),j=Y(`/admin/api/logins/devices${o}`,{enabled:s==="devices"}),p=((x=u.data)==null?void 0:x.users)??(($=j.data)==null?void 0:$.users)??[],g=((k=u.data)==null?void 0:k.totals)??((R=j.data)==null?void 0:R.totals),b=((I=u.data)==null?void 0:I.retentionDays)??((A=j.data)==null?void 0:A.retentionDays)??90,h=s==="log"?u.loading:j.loading,m=s==="log"?u.error:j.error,y=L=>{l(X=>({...X,...L})),d(0)},v=Object.entries(i).some(([,L])=>L!=="")||!!i.from;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Sign-in history",intro:"Every connection attempt, kept as history rather than as the latest state of a device. A television that has since been removed still appears here, because it still connected.",actions:e.jsx(Ue,{value:s,options:[{value:"log",label:"Log"},{value:"devices",label:"By device"}],onChange:n})}),e.jsx(P,{message:m}),g?e.jsx(ae,{tiles:[{label:"Successful sign-ins",value:w(g.logins),icon:"key",tone:"ok"},{label:"Refused",value:w(g.failures),icon:"shield",tone:g.failures>0?"warn":void 0},{label:"Televisions",value:w(g.devices),icon:"tv",tone:"info"},{label:"People",value:w(g.users),icon:"people",tone:"note"},{label:"Addresses",value:w(g.addresses),icon:"globe",tone:"data"},{label:"History kept",value:`${b} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Ue,{value:i.from?-1:t,options:$t.map(L=>({value:L.value,label:L.label})),onChange:L=>{a(L),y({from:"",to:""})}})}),e.jsx(D,{label:"Person",children:e.jsxs("select",{value:i.user,onChange:L=>y({user:L.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),p.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:i.outcome,onChange:L=>y({outcome:L.target.value}),children:[e.jsx("option",{value:"",children:"Both"}),e.jsx("option",{value:"success",children:"Got in"}),e.jsx("option",{value:"failure",children:"Refused"})]})}),e.jsx(D,{label:"Address",children:e.jsx("input",{type:"text",value:i.ip,placeholder:"10.0.0.4",onChange:L=>y({ip:L.target.value})})}),e.jsx(D,{label:"From",children:e.jsx("input",{type:"date",value:i.from,onChange:L=>y({from:L.target.value})})}),e.jsx(D,{label:"To",children:e.jsx("input",{type:"date",value:i.to,onChange:L=>y({to:L.target.value})})}),e.jsx(D,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:i.q,placeholder:"Name, device or address",onChange:L=>y({q:L.target.value})})}),e.jsx("div",{className:"filter-actions",children:v?e.jsx(T,{variant:"quiet",size:"sm",onClick:()=>{l(Ws),d(0)},children:"Clear"}):null})]}),h?e.jsx(B,{}):s==="log"?e.jsx(Et,{data:u.data,page:r,limit:c,onPage:d}):e.jsx(At,{data:j.data})]})}function Et({data:s,page:n,limit:t,onPage:a}){if(!s)return null;const i=s.events.length,l=s.total===0?0:n*t+1;return e.jsxs(e.Fragment,{children:[e.jsxs(ue,{cols:"wide",children:[e.jsx(M,{title:"Attempts per day",intro:"Grouped in the server's own timezone, so an evening sign-in stays on the day it happened.",icon:"chart",tone:"info",children:e.jsx(hs,{data:s.days,labelOf:r=>r.day,valueOf:r=>r.logins+r.failures,toneOf:r=>r.failures>r.logins?"bad":void 0,title:r=>`${r.day}: ${r.logins} in, ${r.failures} refused, ${r.devices} televisions`})}),e.jsx(M,{title:"Where from",icon:"globe",tone:"data",children:s.addresses.length===0?e.jsx(ee,{children:"No addresses in this window."}):e.jsx("div",{className:"list",children:s.addresses.slice(0,8).map(r=>e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:r.ipAddress}),e.jsxs("p",{children:[w(r.logins)," in",r.failures>0?` · ${w(r.failures)} refused`:""]})]}),r.failures>0&&r.logins===0?e.jsx(N,{tone:"bad",children:"only refused"}):null]},r.ipAddress))})})]}),e.jsx(M,{title:"Attempts",intro:"Uncollapsed and newest first: this is what to read when somebody says a television will not sign in.",icon:"key",tone:"ok",actions:e.jsx("span",{className:"filter-summary",children:s.total===0?"nothing matches":`${w(l)}–${w(l+i-1)} of ${w(s.total)}`}),footer:s.total>t?e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",disabled:n===0,onClick:()=>a(n-1),children:"Newer"}),e.jsx(T,{size:"sm",disabled:(n+1)*t>=s.total,onClick:()=>a(n+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Person"}),e.jsx("th",{children:"Television"}),e.jsx("th",{className:"nowrap",children:"Address"}),e.jsx("th",{children:"Build"}),e.jsx("th",{children:"Outcome"})]})}),e.jsx("tbody",{children:s.events.length===0?e.jsx(J,{columns:6,children:"No sign-in attempts match these filters."}):s.events.map(r=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(r.occurredAt)}),e.jsx("td",{children:r.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{children:r.deviceId?e.jsx(G,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(r.deviceId)}`,children:r.deviceName||r.deviceId}):e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"mono nowrap",children:r.ipAddress||"—"}),e.jsx("td",{className:"mono",children:r.clientVersion||"—"}),e.jsx("td",{className:"nowrap",children:r.success?r.newDevice?e.jsx(N,{tone:"info",children:"first sign-in"}):e.jsx(N,{tone:"ok",children:"got in"}):e.jsx(N,{tone:"bad",children:r.failureReason||"refused"})})]},r.id))})]})})})]})}function At({data:s}){return s?e.jsx(M,{title:"Televisions",intro:"Grouped from the history, not from the session list — a set whose session has expired still connected, and this is the record of it.",icon:"tv",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Television"}),e.jsx("th",{children:"Person"}),e.jsx("th",{className:"num",children:"Today"}),e.jsx("th",{className:"num",children:"Sign-ins"}),e.jsx("th",{className:"num",children:"Refused"}),e.jsx("th",{className:"num",children:"Addresses"}),e.jsx("th",{className:"nowrap",children:"Last address"}),e.jsx("th",{className:"nowrap",children:"Last sign-in"}),e.jsx("th",{children:"Build"})]})}),e.jsx("tbody",{children:s.devices.length===0?e.jsx(J,{columns:9,children:"No television has connected in this window."}):s.devices.map(n=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(G,{className:"table-row-link",to:`/admin/devices/${encodeURIComponent(n.deviceId)}`,children:n.deviceName||n.deviceId})}),e.jsx("td",{className:"muted",children:n.username||"—"}),e.jsx("td",{className:"num",children:n.loginsToday>0?w(n.loginsToday):"—"}),e.jsx("td",{className:"num",children:w(n.logins)}),e.jsx("td",{className:"num",children:n.failures>0?e.jsx("span",{className:"mono",children:w(n.failures)}):"—"}),e.jsx("td",{className:"num",children:w(n.distinctIps)}),e.jsx("td",{className:"mono nowrap",children:n.lastIp||"—"}),e.jsx("td",{className:"nowrap muted",children:n.lastLogin?F(n.lastLogin):"—"}),e.jsx("td",{className:"mono",children:n.clientVersion||"—"})]},n.deviceId))})]})})}):null}const It=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:0,label:"All"}];function Lt(){const{deviceId:s=""}=Be(),[n,t]=f.useState(7),a=f.useMemo(()=>`/admin/api/logins/devices/${encodeURIComponent(s)}${Ve({days:n||void 0,limit:200})}`,[s,n]),{data:i,error:l,loading:r}=Y(a,{enabled:!!s}),d=i==null?void 0:i.summary,c=(d==null?void 0:d.deviceName)||s;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:c,intro:"One television's whole relationship with the gateway.",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(G,{to:"/admin/clients",children:"Devices"}),e.jsx("span",{children:"/"}),e.jsx(G,{to:"/admin/logins",children:"Sign-ins"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:c})]}),actions:e.jsx(Ue,{value:n,options:It.map(o=>({value:o.value,label:o.label})),onChange:t})}),e.jsx(P,{message:l}),r?e.jsx(B,{}):i?e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Sign-ins today",value:w((d==null?void 0:d.loginsToday)??0),icon:"clock",tone:"ok"},{label:"Sign-ins in total",value:w((d==null?void 0:d.logins)??0),icon:"key",tone:"info"},{label:"Refused",value:w((d==null?void 0:d.failures)??0),icon:"shield",tone:((d==null?void 0:d.failures)??0)>0?"warn":void 0},{label:"Addresses seen",value:w((d==null?void 0:d.distinctIps)??0),icon:"globe",tone:"data"},{label:"First seen",value:d!=null&&d.firstLogin?F(d.firstLogin):"—",small:!0,icon:"history"},{label:"Last seen",value:d!=null&&d.lastLogin?F(d.lastLogin):"—",small:!0,icon:"pulse",tone:"note"}]}),e.jsxs(ue,{cols:"wide",children:[e.jsx(M,{title:"Connections per day",icon:"chart",tone:"info",children:e.jsx(hs,{data:i.days,labelOf:o=>o.day,valueOf:o=>o.logins+o.failures,toneOf:o=>o.failures>o.logins?"bad":void 0,title:o=>`${o.day}: ${o.logins} in${o.failures?`, ${o.failures} refused`:""}`})}),e.jsxs("div",{className:"stack",children:[e.jsx(M,{title:"Identity",icon:"tv",tone:"info",children:e.jsxs("div",{className:"list",children:[e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Person"}),e.jsx("p",{children:(d==null?void 0:d.username)||"unknown"})]})}),e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Device id"}),e.jsx("p",{className:"mono",children:i.deviceId})]})}),e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Running"}),e.jsx("p",{className:"mono",children:(d==null?void 0:d.clientVersion)||"unknown"})]})})]})}),e.jsx(M,{title:"Builds",intro:"Kept per television rather than per session, so it survives a sign-out.",icon:"upload",tone:"note",children:i.versions.length===0?e.jsx(ee,{children:"No build history for this television."}):e.jsx("div",{className:"list",children:i.versions.map(o=>e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{className:"mono",children:o.version}),e.jsxs("p",{children:[F(o.firstSeen)," → ",F(o.lastSeen)]})]})},o.version))})})]})]}),e.jsx(M,{title:"Addresses",icon:"globe",tone:"data",children:i.addresses.length===0?e.jsx(ee,{children:"No addresses recorded in this window."}):e.jsx("div",{className:"chips",children:i.addresses.map(o=>e.jsxs(oe,{tone:o.failures>0?"warn":"data",children:[o.ipAddress," · ",w(o.logins),o.failures>0?` (+${w(o.failures)} refused)`:""]},o.ipAddress))})}),e.jsx(M,{title:"Every attempt",icon:"key",tone:"ok",actions:e.jsxs("span",{className:"filter-summary",children:[w(i.total)," in this window"]}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Person"}),e.jsx("th",{className:"nowrap",children:"Address"}),e.jsx("th",{children:"Build"}),e.jsx("th",{children:"Method"}),e.jsx("th",{children:"Outcome"})]})}),e.jsx("tbody",{children:i.events.length===0?e.jsx(J,{columns:6,children:"This television has not connected in the selected window."}):i.events.map(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(o.occurredAt)}),e.jsx("td",{children:o.username||e.jsx("span",{className:"quiet",children:"unknown"})}),e.jsx("td",{className:"mono nowrap",children:o.ipAddress||"—"}),e.jsx("td",{className:"mono",children:o.clientVersion||"—"}),e.jsx("td",{className:"muted",children:o.method}),e.jsx("td",{className:"nowrap",children:o.success?o.newDevice?e.jsx(N,{tone:"info",children:"first sign-in"}):e.jsx(N,{tone:"ok",children:"got in"}):e.jsx(N,{tone:"bad",children:o.failureReason||"refused"})})]},o.id))})]})})})]}):null]})}function Dt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(!1),o=(s==null?void 0:s.library.byType)??{},u=!!(s!=null&&s.syncRunning),j=p=>r(p,async()=>{await i(()=>q.post("/admin/api/sync",{kind:p}),p==="full"?"Full re-import started.":"Import started."),c(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Library",intro:"Import and inspect the catalogue Memby ranks."}),e.jsx(P,{message:n}),t||!s?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"items",value:w(s.library.total),icon:"library",tone:"data"},...Object.keys(o).sort().map(p=>({label:p,value:w(o[p]),icon:"list"})),{label:"last import",value:F(s.library.lastSynced),small:!0,icon:"clock"}]}),e.jsx(M,{title:"Import the catalogue",intro:"Emby's catalogue is copied here so search and the recommendation candidate pool can be answered from one indexed table. Watched, favourite and resume state is deliberately not stored — that is per person and still comes from Emby live.",icon:"library",tone:"data",footer:e.jsx("span",{className:"hint",children:u?"Import running…":`An incremental import runs automatically every ${s.syncEvery}.`}),children:e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",icon:"sync",disabled:u,busy:l==="incremental",onClick:()=>void j("incremental"),children:"Sync new items"}),e.jsx(T,{icon:"database",disabled:u,onClick:()=>c(!0),children:"Full re-import"})]})})]}),d?e.jsx(ke,{title:"Re-import the entire library?",body:"A full pass mark-and-sweeps the catalogue and can take several minutes on a large library. Televisions keep reading the current table throughout.",confirmLabel:"Re-import",busy:l==="full",onConfirm:()=>void j("full"),onCancel:()=>c(!1)}):null]})}function qt(){var j;const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),d=(s==null?void 0:s.requestUsers)??[],c=((j=s==null?void 0:s.requestPolicy)==null?void 0:j.allowedUserIds)??[],o=f.useMemo(()=>new Map(((s==null?void 0:s.requestUsage)??[]).map(p=>[p.userId,p])),[s==null?void 0:s.requestUsage]),u=p=>r(`access-${p}`,async()=>{const g=c.includes(p)?c.filter(b=>b!==p):[...c,p];await i(()=>q.post("/admin/api/request-policy",{allowedUserIds:g}),g.includes(p)?"Request access granted.":"Request access removed."),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Media requests",intro:"Who can ask for something the library does not have."}),e.jsx(P,{message:n}),t?e.jsx(B,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Where a request goes",intro:"Movies follow Radarr’s policy. Series follow the dedicated Sonarr request policy: monitored as normal, with backlog searching only when an operator enables it under Integrations.",icon:"inbox",tone:"info",actions:e.jsxs(e.Fragment,{children:[e.jsxs(N,{tone:s!=null&&s.radarrReady?"ok":"bad",children:["Movies ",s!=null&&s.radarrReady?"ready":"not configured"]}),e.jsxs(N,{tone:s!=null&&s.sonarrReady?"ok":"bad",children:["Series ",s!=null&&s.sonarrReady?"ready":"not configured"]})]}),children:!(s!=null&&s.radarrReady)&&!(s!=null&&s.sonarrReady)?e.jsx(ee,{children:"Neither Radarr nor Sonarr is configured, so a request would have nowhere to go. The button stays hidden on every television until one of them is."}):null}),e.jsx(M,{title:"Request access and activity",intro:"One button grants or removes access. A recorded request has already been sent to Radarr or Sonarr; Memby does not duplicate their download state.",icon:"people",tone:"note",children:d.length===0?e.jsx(ee,{children:"No one has signed in yet."}):e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"User"}),e.jsx("th",{children:"Last seen"}),e.jsx("th",{className:"num",children:"Sent to services"}),e.jsx("th",{children:"Last request"}),e.jsx("th",{children:"Access"})]})}),e.jsx("tbody",{children:d.map(p=>{const g=o.get(p.id),b=c.includes(p.id);return e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx("b",{children:p.username})}),e.jsx("td",{className:"muted nowrap",children:F(p.lastSeen)}),e.jsx("td",{className:"num",children:(g==null?void 0:g.requests)??0}),e.jsx("td",{className:"muted nowrap",children:g!=null&&g.lastRequest?F(g.lastRequest):"—"}),e.jsx("td",{children:e.jsx(T,{size:"sm",variant:b?"quiet":"primary",busy:l===`access-${p.id}`,onClick:()=>void u(p.id),children:b?"Remove access":"Give access"})})]},p.id)})})]})})})]})]})}function Ft(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(!1),o=s==null?void 0:s.forYou,u=!!(s!=null&&s.forYouRunning),j=(p,g,b)=>r(g,async()=>{await i(()=>q.post("/admin/api/for-you",{action:p}),b),c(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"For You",intro:"The prepared pools personalised rows are drawn from."}),e.jsx(P,{message:n}),t?e.jsx(B,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Tracearr sessions",value:w((o==null?void 0:o.tracearrSessions)??0),icon:"play",tone:"info"},{label:"user profiles",value:w((o==null?void 0:o.profiles)??0),icon:"people",tone:"note"},{label:"ranked candidates",value:w((o==null?void 0:o.candidates)??0),icon:"sparkle",tone:"note"},{label:"last full import",value:F(o==null?void 0:o.lastFullImport),small:!0,icon:"clock"}]}),e.jsx(M,{title:"Pool maintenance",intro:"Prepared pools refresh in the background; these are the manual versions of the same work. A rebuild is safe at any time — televisions read the last finished pool until a new one lands.",icon:"sparkle",tone:"note",footer:e.jsx("span",{className:"hint",children:u?"For You maintenance running…":"Prepared pools normally refresh in the background."}),children:e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",icon:"download",disabled:u,busy:l==="import",onClick:()=>void j("incremental-import","import","Import started."),children:"Import recent sessions"}),e.jsx(T,{icon:"database",disabled:u,onClick:()=>c(!0),children:"Full Tracearr backfill"}),e.jsx(T,{icon:"sync",disabled:u,busy:l==="rebuild",onClick:()=>void j("rebuild-all","rebuild","Rebuild started."),children:"Rebuild all pools"})]})}),e.jsx(M,{title:"Reading a person's scores",intro:"The inspector re-runs the shared weighted scorer over one person's prepared pool, after Emby permission and parental-control filtering, and shows every component and evidence reason behind the order.",icon:"search",tone:"info",actions:e.jsx(G,{to:"/admin/inspector",children:"Open the inspector"}),children:e.jsx(e.Fragment,{})})]}),d?e.jsx(ke,{title:"Backfill all Tracearr history?",body:"Every session is re-read and every active user's pool is rebuilt. It is safe at any time — televisions keep reading the last finished pool — but on a long history it takes a while.",confirmLabel:"Backfill",busy:l==="full",onConfirm:()=>void j("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 Pt(s){return s?Ot.flatMap(([n,t])=>Object.entries(s[t]??{}).map(([a,i])=>({dimension:n,name:a,weight:i.weight??0,evidence:i.evidence??0}))).sort((n,t)=>Math.abs(t.weight)-Math.abs(n.weight)):[]}const Bs=s=>`${s>=0?"+":""}${s.toFixed(3)}`;function _t(){const{status:s}=je(),{wrap:n}=te(),{busy:t,run:a}=se(),[i,l]=f.useState(""),[r,d]=f.useState("default"),[c,o]=f.useState("0"),[u,j]=f.useState(""),[p,g]=f.useState(null),[b,h]=f.useState("Choose a person to inspect their recommendations."),[m,y]=f.useState(""),v=(s==null?void 0:s.requestUsers)??[],x=()=>a("run",async()=>{if(!i){y("Choose a person to pressure-test.");return}y(""),h("Running the permission check and the scorer…");const A=new URLSearchParams({userId:i,context:r,minutes:c||"0",limit:"100"});u&&A.set("at",new Date(u).toISOString());const L=await n(()=>q.get(`/admin/api/recommendations?${A.toString()}`));L?(g(L),h(`Scored at ${new Date().toLocaleTimeString()}.`)):h("Pressure test failed.")}),$=Pt((p==null?void 0:p.profile)??null).slice(0,24),k=(p==null?void 0:p.actions)??[],R=(p==null?void 0:p.items)??[],I=(p==null?void 0:p.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(P,{message:m}),e.jsx(M,{title:"Run a pressure test",intro:"Nothing is changed by running this. It scores the person's prepared pool as the launcher would, in the context you choose.",icon:"search",tone:"info",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:t==="run",onClick:()=>void x(),children:"Run pressure test"}),e.jsx("span",{className:"hint",children:b})]}),children:e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Person",children:e.jsxs("select",{value:i,onChange:A=>l(A.target.value),children:[e.jsx("option",{value:"",children:"Choose a person…"}),v.map(A=>e.jsx("option",{value:A.id,children:A.username},A.id))]})}),e.jsx(D,{label:"Context",children:e.jsxs("select",{value:r,onChange:A=>d(A.target.value),children:[e.jsx("option",{value:"default",children:"Default"}),e.jsx("option",{value:"bedtime",children:"One episode before bed"}),e.jsx("option",{value:"hidden",children:"Hidden library"}),e.jsx("option",{value:"new-releases",children:"Recent new releases"})]})}),e.jsx(D,{label:"Available minutes",children:e.jsx("input",{type:"number",min:0,max:360,value:c,onChange:A=>o(A.target.value)})}),e.jsx(D,{label:"Evaluate at",children:e.jsx("input",{type:"datetime-local",value:u,onChange:A=>j(A.target.value)})})]})}),p?e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"prepared pool",value:w(p.poolCandidates),icon:"database",tone:"data"},{label:"permission eligible",value:w(p.permissionEligible),icon:"shield",tone:"ok"},{label:"ranked result",value:w(R.length),icon:"sparkle",tone:"note"},{label:"source events",value:w(I.sourceEvents??0),icon:"pulse",tone:"info"},{label:"algorithm",value:I.algorithmVersion||"—",small:!0,icon:"chip"},{label:"pool built",value:F(I.poolBuiltAt),small:!0,icon:"clock"}]}),e.jsxs(M,{title:"Profile evidence",intro:"The strongest learned affinities, and every explicit action this person has taken.",icon:"sparkle",tone:"note",children:[$.length===0?e.jsx(ee,{children:"No repeated affinity evidence yet; cold-start priors apply."}):e.jsx("div",{className:"chips",children:$.map(A=>e.jsxs(oe,{tone:A.weight<0?"bad":void 0,children:[A.dimension,": ",A.name," ",Bs(A.weight)," · n=",w(A.evidence)]},`${A.dimension}:${A.name}`))}),k.length===0?e.jsx(ee,{children:"No explicit recommendation actions."}):e.jsx("div",{className:"chips",children:k.map((A,L)=>e.jsxs(oe,{tone:"ok",children:[A.action,": ",A.title||A.itemId]},`${A.action}:${L}`))})]}),R.length===0?e.jsx(M,{children:e.jsx(ee,{children:"No candidates survived this context, the explicit exclusions and the permission filter."})}):e.jsx(ue,{children:R.map((A,L)=>{const X=A.explanation??{},le=Object.entries(X.components??{}).sort((H,re)=>Math.abs(re[1])-Math.abs(H[1])),me=A.exposure??{},ie=[A.type,A.year,A.runtimeMinutes?`${A.runtimeMinutes} min`:null,...A.genres??[]].filter(Boolean).join(" · ");return e.jsxs(M,{title:`#${L+1} · ${A.title}`,intro:ie,actions:e.jsx(oe,{tone:"ok",children:Number(X.total??0).toFixed(3)}),children:[e.jsxs("p",{className:"hint",children:[A.preparedReason||"No legacy prepared explanation",A.compatibilityLabel?` · ${A.compatibilityLabel}`:""]}),e.jsxs("div",{className:"chips",children:[(X.reasonCodes??[]).map(H=>e.jsx(oe,{tone:"ok",children:H},H)),le.map(([H,re])=>e.jsxs(oe,{tone:re<0?"bad":void 0,children:[H,"=",Bs(re)]},H))]}),e.jsxs("details",{children:[e.jsx("summary",{className:"muted",children:"Pool, row and exposure detail"}),e.jsxs("p",{className:"hint",children:["Base rank ",w(A.baseRank??0)," · base ",Number(A.baseScore??0).toFixed(3)," · affinity ",Number(A.affinityScore??0).toFixed(3)," · compatibility"," ",Number(A.compatibilityScore??0).toFixed(3)," · impressions"," ",w(me.impressions??0)," · focuses ",w(me.focuses??0)," · selects"," ",w(me.selects??0)]}),e.jsx("div",{className:"chips",children:(A.eligibleRows??[]).map(H=>e.jsx(oe,{tone:"ok",children:H},H))}),A.preparedEvidenceTitle?e.jsxs("p",{className:"hint",children:["Prepared evidence: ",A.preparedEvidenceTitle]}):null]})]},`${L}:${A.title}`)})})]}):null]})}const Ut=4,Ae=[{id:"home",label:"Home",type:"films and television shows"},{id:"movies",label:"Movies",type:"films"},{id:"tv_shows",label:"TV Shows",type:"television shows"}],ze=()=>({pinnedItems:[],primeSubtitle:""}),fs=[{value:1,short:"Mon",label:"Monday"},{value:2,short:"Tue",label:"Tuesday"},{value:3,short:"Wed",label:"Wednesday"},{value:4,short:"Thu",label:"Thursday"},{value:5,short:"Fri",label:"Friday"},{value:6,short:"Sat",label:"Saturday"},{value:0,short:"Sun",label:"Sunday"}];function Vs(s){const n=s.getTimezoneOffset()*6e4;return new Date(s.getTime()-n).toISOString().slice(0,16)}function Hs(s){return!!(s&&Number.isFinite(new Date(s).getTime())&&new Date(s).getFullYear()>=2e3)}function Wt(s){return s.frequency??"once"}function Bt(s){if(s.frequency==="daily")return`Every day · ${s.startTime}–${s.endTime}`;if(s.frequency==="weekly"){const n=fs.filter(a=>(s.weekdays??[]).includes(a.value));return`${(n.length===7?"Every day":n.map(a=>a.short).join(", "))||"No days selected"} · ${s.startTime}–${s.endTime}`}return`${s.startAt?new Date(s.startAt).toLocaleString():"Start missing"} → ${s.endAt?new Date(s.endAt).toLocaleString():"End missing"}`}function zs(s,n){const t=new Date;t.setMinutes(Math.ceil(t.getMinutes()/30)*30,0,0);const a=new Date(t.getTime()+2*60*60*1e3),i=n==="home"||n==="movies"&&s.type==="Movie"||n==="tv_shows"&&s.type==="Series";return{id:crypto.randomUUID(),itemId:s.id,startAt:t.toISOString(),endAt:a.toISOString(),priority:0,enabled:!0,placements:[i?n:"home"]}}function Vt(){var ie,H,re;const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i,show:l}=te(),{busy:r,run:d}=se(),[c,o]=f.useState("home"),[u,j]=f.useState({home:ze(),movies:ze(),tv_shows:ze()}),[p,g]=f.useState(!1),[b,h]=f.useState(""),[m,y]=f.useState(null),[v,x]=f.useState([]),[$,k]=f.useState(null),R=s==null?void 0:s.heroPolicy;f.useEffect(()=>{var S,O,z;p||!R||(j({home:((S=R.placements)==null?void 0:S.home)??{pinnedItems:R.pinnedItems??[],primeSubtitle:R.primeSubtitle??""},movies:((O=R.placements)==null?void 0:O.movies)??ze(),tv_shows:((z=R.placements)==null?void 0:z.tv_shows)??ze()}),x(R.schedules??[]))},[R,p]);const I=()=>d("search",async()=>{const S=b.trim();if(!S)return;const O=await i(()=>q.get(`/admin/api/hero/search?q=${encodeURIComponent(S)}`));O&&y(O.items??[])}),A=u[c],L=A.pinnedItems??[],X=S=>{j(O=>({...O,[c]:{...O[c],...S}})),g(!0)},le=S=>{if(!L.some(O=>O.id===S.id)){if(L.length>=Ut){l("Remove a pinned title before adding another.","bad");return}X({pinnedItems:[...L,S]})}},me=()=>d("save",async()=>{await i(()=>q.post("/admin/api/hero-policy",{placements:Object.fromEntries(Object.entries(u).map(([S,O])=>[S,{pinnedItemIds:(O.pinnedItems??[]).map(z=>z.id),primeSubtitle:O.primeSubtitle.trim()}])),schedules:v}),"Hero saved."),g(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Featured content",intro:"Manage an independent, backend-resolved hero for Home, Movies and TV Shows."}),e.jsx(P,{message:n}),t?e.jsx(B,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(ue,{children:Ae.map(S=>{var Re;const O=u[S.id],z=v.filter(ge=>ge.enabled&&(ge.placements??["home"]).includes(S.id)).sort((ge,E)=>E.priority-ge.priority)[0],ce=(O.pinnedItems??[]).length?"Manual":z?"Schedule ready":"Automatic",we=(Re=R==null?void 0:R.items)==null?void 0:Re.find(ge=>ge.id===(z==null?void 0:z.itemId)),$e=(O.pinnedItems??[]).map(ge=>ge.name).join(", ")||(we==null?void 0:we.name)||(z==null?void 0:z.itemId)||"Resolved for each viewer";return e.jsx(M,{title:S.label,intro:`${ce} · ${$e}`,tone:S.id===c?"info":void 0,children:e.jsxs(T,{size:"sm",variant:"quiet",onClick:()=>o(S.id),children:["Manage ",S.label]})},S.id)})}),e.jsx("div",{className:"tabs",role:"tablist","aria-label":"Hero placement",children:Ae.map(S=>e.jsx(T,{variant:c===S.id?"primary":"quiet",onClick:()=>o(S.id),children:S.label},S.id))}),e.jsxs(M,{title:`${(ie=Ae.find(S=>S.id===c))==null?void 0:ie.label} hero`,intro:`Pinned ${(H=Ae.find(S=>S.id===c))==null?void 0:H.type} lead this section only. Empty places use this placement’s automatic selection.`,icon:"star",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:r==="save",onClick:()=>void me(),children:"Save hero"}),e.jsx(T,{onClick:()=>{X({pinnedItems:[]})},children:"Clear pins"}),p?e.jsx("span",{className:"hint",children:"Unsaved changes."}):null]}),children:[L.length===0?e.jsx(ee,{children:"No titles are pinned. The hero is entirely release-aware and automatic."}):e.jsx("div",{className:"hero-pins",children:L.map((S,O)=>e.jsxs("div",{className:"hero-pin",children:[e.jsx("span",{className:"hero-pin-order",children:O+1}),e.jsxs("span",{children:[e.jsx("b",{children:S.name}),e.jsxs("small",{children:[S.type,S.year?` · ${S.year}`:""]})]}),e.jsx(T,{size:"sm",icon:"clock",onClick:()=>k(zs(S,c)),children:"Schedule"}),e.jsx(T,{variant:"quiet",size:"sm",icon:"close",title:`Remove ${S.name}`,onClick:()=>X({pinnedItems:L.filter(z=>z.id!==S.id)})})]},S.id))}),e.jsx(D,{label:"Prime-card subtitle",hint:"Optional wording under the large first card. Leave blank to use Memby's natural release or rating reason.",children:e.jsx("input",{type:"text",maxLength:160,value:A.primeSubtitle,placeholder:"Leave blank for the automatic reason",onChange:S=>{X({primeSubtitle:S.target.value})}})})]}),e.jsx(M,{title:"Hero schedule",intro:"The gateway applies these rules in server time. Manual pins win first; otherwise the highest-priority active schedule wins, followed by Memby’s automatic hero.",icon:"clock",tone:"info",actions:e.jsx(N,{tone:"info",children:(R==null?void 0:R.timeZone)||"server local time"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:r==="save",onClick:()=>void me(),children:"Save schedule"}),e.jsx("span",{className:"hint",children:"Daily and weekly rules repeat until you switch them off."})]}),children:v.length===0?e.jsx(ee,{children:"No scheduled heroes yet. Use Schedule beside a pinned or searched title."}):e.jsx("div",{className:"hero-schedule-list",children:[...v].sort((S,O)=>Number(O.enabled)-Number(S.enabled)||O.priority-S.priority).map(S=>{const O=[...(R==null?void 0:R.items)??[],...L,...m??[]].find(z=>z.id===S.itemId);return e.jsxs("article",{className:"hero-schedule","data-enabled":S.enabled||void 0,children:[e.jsxs("div",{className:"hero-schedule-time",children:[e.jsx("b",{children:S.frequency==="weekly"?"Weekly":S.frequency==="daily"?"Daily":"Once"}),e.jsx("span",{children:S.frequency?S.startTime:S.startAt?new Date(S.startAt).toLocaleDateString():"—"})]}),e.jsxs("div",{className:"hero-schedule-main",children:[e.jsxs("div",{className:"hero-schedule-title",children:[e.jsx("h3",{children:(O==null?void 0:O.name)??S.itemId}),e.jsx(N,{tone:S.enabled?"ok":void 0,children:S.enabled?"enabled":"paused"})]}),e.jsx("p",{children:Bt(S)}),e.jsxs("div",{className:"chips",children:[(S.placements??["home"]).map(z=>{var ce;return e.jsx("span",{className:"chip",children:(ce=Ae.find(we=>we.id===z))==null?void 0:ce.label},z)}),S.priority!==0?e.jsxs("span",{className:"chip",children:["Priority ",S.priority]}):null]})]}),e.jsxs("div",{className:"hero-schedule-actions",children:[e.jsx(T,{size:"sm",onClick:()=>k({...S}),children:"Edit"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>{x(z=>z.map(ce=>ce.id===S.id?{...ce,enabled:!ce.enabled}:ce)),g(!0)},children:S.enabled?"Pause":"Enable"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>{x(z=>z.filter(ce=>ce.id!==S.id)),g(!0)},children:"Remove"})]})]},S.id)})})}),e.jsxs(M,{title:"Find a title",intro:`Search the imported Emby catalogue. Add a result to the selected ${(re=Ae.find(S=>S.id===c))==null?void 0:re.label} placement; switch tabs to show it in more than one section.`,icon:"search",tone:"info",children:[e.jsxs("div",{className:"field-row",children:[e.jsx(D,{label:"Title",grow:!0,children:e.jsx("input",{type:"search",value:b,placeholder:"Search films and television shows",onChange:S=>h(S.target.value),onKeyDown:S=>{S.key==="Enter"&&I()}})}),e.jsx(T,{busy:r==="search",icon:"search",onClick:()=>void I(),children:"Search"})]}),m===null?null:m.length===0?e.jsx(ee,{children:"No playable films or series matched that search."}):e.jsx(ue,{children:m.map(S=>e.jsx(M,{title:S.name,intro:`${S.type||"Title"} · ${S.year||"Year unknown"}`,children:e.jsxs("div",{className:"row",children:[e.jsx(T,{size:"sm",icon:"plus",disabled:L.some(O=>O.id===S.id)||c==="movies"&&S.type!=="Movie"||c==="tv_shows"&&S.type!=="Series",onClick:()=>le(S),children:L.some(O=>O.id===S.id)?"Pinned":"Add to hero"}),e.jsx(T,{size:"sm",icon:"clock",onClick:()=>k(zs(S,c)),children:"Schedule"})]})},S.id))})]})]}),$?e.jsx(Ht,{schedule:$,item:[...(R==null?void 0:R.items)??[],...L,...m??[]].find(S=>S.id===$.itemId),timeZone:(R==null?void 0:R.timeZone)||"server local time",isNew:!v.some(S=>S.id===$.id),onCancel:()=>k(null),onSave:S=>{x(O=>O.some(z=>z.id===S.id)?O.map(z=>z.id===S.id?S:z):[...O,S]),k(null),g(!0)}}):null]})}function Ht({schedule:s,item:n,timeZone:t,isNew:a,onSave:i,onCancel:l}){const[r,d]=f.useState({...s,weekdays:[...s.weekdays??[]]}),c=Wt(r),o=h=>{const m=new Date,y=new Date(m.getTime()+2*60*60*1e3);d(v=>{var x;return h==="once"?{...v,frequency:void 0,startAt:Hs(v.startAt)?v.startAt:m.toISOString(),endAt:Hs(v.endAt)?v.endAt:y.toISOString()}:{...v,frequency:h,startTime:v.startTime||"18:00",endTime:v.endTime||"22:00",weekdays:h==="weekly"?(x=v.weekdays)!=null&&x.length?v.weekdays:[1,2,3,4,5]:[]}})},u=r.placements??["home"],j=h=>h==="home"||h==="movies"&&(n==null?void 0:n.type)==="Movie"||h==="tv_shows"&&(n==null?void 0:n.type)==="Series",p=!!(r.startAt&&r.endAt&&new Date(r.endAt)>new Date(r.startAt)),g=!!(r.startTime&&r.endTime&&r.startTime!==r.endTime&&(c!=="weekly"||(r.weekdays??[]).length>0)),b=c==="once"?p:g;return e.jsx("div",{className:"scrim",onPointerDown:h=>h.target===h.currentTarget&&l(),children:e.jsxs("div",{className:"dialog hero-schedule-dialog",role:"dialog","aria-modal":"true","aria-labelledby":"hero-schedule-title",children:[e.jsxs("div",{className:"hero-schedule-dialog-head",children:[e.jsx("span",{className:"hero-schedule-kicker",children:"Hero schedule"}),e.jsx("h2",{id:"hero-schedule-title",children:(n==null?void 0:n.name)??r.itemId}),e.jsxs("p",{children:["Choose exactly when this title can lead the selected sections. Times use ",t,"."]})]}),e.jsx("div",{className:"schedule-frequency",role:"group","aria-label":"Schedule frequency",children:["once","daily","weekly"].map(h=>e.jsxs("button",{type:"button","aria-pressed":c===h,onClick:()=>o(h),children:[e.jsx("b",{children:h==="once"?"One time":h==="daily"?"Every day":"Weekly"}),e.jsx("span",{children:h==="once"?"A date range":h==="daily"?"Same time daily":"Choose days"})]},h))}),c==="once"?e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts",children:e.jsx("input",{type:"datetime-local",value:r.startAt?Vs(new Date(r.startAt)):"",onChange:h=>d(m=>({...m,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:r.endAt?Vs(new Date(r.endAt)):"",onChange:h=>d(m=>({...m,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:r.startTime??"",onChange:h=>d(m=>({...m,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:r.endTime??"",onChange:h=>d(m=>({...m,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:fs.map(m=>m.value)})),children:"Every day"})]})]}),e.jsx("div",{className:"schedule-day-grid",children:fs.map(h=>{const m=(r.weekdays??[]).includes(h.value);return e.jsx("button",{type:"button","aria-pressed":m,title:h.label,onClick:()=>d(y=>({...y,weekdays:m?(y.weekdays??[]).filter(v=>v!==h.value):[...y.weekdays??[],h.value]})),children:h.short},h.value)})})]}):null]}),e.jsxs("div",{className:"schedule-options",children:[e.jsxs("div",{children:[e.jsx("span",{className:"schedule-option-label",children:"Show in"}),e.jsx("div",{className:"schedule-placement-grid",children:Ae.map(h=>e.jsx(Z,{label:h.label,checked:u.includes(h.id),disabled:!j(h.id),onChange:m=>d(y=>{const v=y.placements??["home"],x=m?[...v,h.id]:v.filter($=>$!==h.id);return{...y,placements:x.length?[...new Set(x)]:v}})},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:r.priority,onChange:h=>d(m=>({...m,priority:Number(h.target.value)}))})})]}),e.jsx(Z,{label:"Schedule enabled",hint:"Pause it without losing its days and times.",checked:r.enabled,onChange:h=>d(m=>({...m,enabled:h}))}),e.jsxs("div",{className:"dialog-actions",children:[e.jsx(T,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx(T,{variant:"primary",disabled:!b,onClick:()=>i(r),children:a?"Add rule":"Save rule"})]})]})})}const Ks=["title","ratings","facts","summary"];function zt(){var b;const s=Y("/admin/api/metadata-hero"),{busy:n,run:t}=se(),{wrap:a}=te(),[i,l]=f.useState(null),[r,d]=f.useState(null);f.useEffect(()=>{i===null&&s.data&&l(s.data.settings.contentOrder),r===null&&s.data&&d(s.data.settings.timeRemainingColour??"green")},[i,s.data,r]);const c=i??Ks,o=r??"green",u=((b=s.data)==null?void 0:b.options)??[],j=[...c,...u.map(h=>h.value).filter(h=>!c.includes(h))],p=(h,m)=>{const y=c.indexOf(h),v=y+m;if(y<0||v<0||v>=c.length)return;const x=[...c],$=x[y];x[y]=x[v],x[v]=$,l(x)},g=()=>t("save",async()=>{const h=await a(()=>q.post("/admin/api/metadata-hero",{contentOrder:c,timeRemainingColour:o}),"Metadata hero saved for every television.");h&&(s.set(h),l(h.settings.contentOrder),d(h.settings.timeRemainingColour))});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Metadata hero",intro:"Choose which content appears beside focused browse cards, and the order every television uses."}),e.jsx(P,{message:s.error}),s.loading?e.jsx(B,{rows:2}):e.jsxs("div",{className:"metadata-hero-layout",children:[e.jsxs(M,{title:"Content order",intro:"Enabled blocks are drawn from top to bottom. Changes apply to the whole household.",icon:"sliders",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:n==="save",onClick:()=>void g(),children:"Save metadata hero"}),e.jsx(T,{onClick:()=>{l(Ks),d("green")},children:"Restore defaults"})]}),children:[e.jsx("div",{className:"metadata-hero-order",children:j.map(h=>{const m=u.find(x=>x.value===h);if(!m)return null;const y=c.indexOf(h),v=y>=0;return e.jsxs("div",{className:"metadata-hero-order-row","data-enabled":v||void 0,children:[e.jsx("span",{className:"metadata-hero-order-number",children:v?y+1:"—"}),e.jsx(Z,{label:m.label,hint:m.description,checked:v,disabled:v&&c.length===1,onChange:x=>l(x?[...c,h]:c.filter($=>$!==h))}),e.jsxs("div",{className:"metadata-hero-order-actions",children:[e.jsx(T,{size:"sm",variant:"quiet",disabled:!v||y===0,title:`Move ${m.label} up`,onClick:()=>p(h,-1),children:"↑"}),e.jsx(T,{size:"sm",variant:"quiet",disabled:!v||y===c.length-1,title:`Move ${m.label} down`,onClick:()=>p(h,1),children:"↓"})]})]},h)})}),e.jsxs("div",{className:"metadata-hero-colour",children:[e.jsx("b",{children:"Time-remaining text colour"}),e.jsx("p",{children:"Choose the caption colour used beside the resume progress bar."}),e.jsx(Ue,{value:o,options:[{value:"green",label:"Emby green"},{value:"white",label:"White"}],onChange:d})]})]}),e.jsx(M,{title:"Television preview",intro:"A representative focused film at launcher scale and in the current order.",icon:"tv",children:e.jsxs("div",{className:"metadata-hero-preview","aria-label":"Metadata hero preview",children:[e.jsx("div",{className:"metadata-hero-preview-copy",children:c.map(h=>h==="title"?e.jsx("div",{className:"metadata-preview-title",children:"THE SOUTHERN LIGHT"},h):h==="ratings"?e.jsxs("div",{className:"metadata-preview-ratings",children:[e.jsx("b",{children:"IMDb 8.2"}),e.jsx("b",{children:"TMDb 81%"})]},h):h==="facts"?e.jsxs("div",{className:"metadata-preview-facts",children:[e.jsx("span",{children:"2026   •   1h 54m   •   M   •   Drama, Mystery"}),e.jsx("b",{children:"4K"}),e.jsx("b",{children:"5.1"})]},h):h==="recommendation_reason"?e.jsx("div",{className:"metadata-preview-reason",children:"Because you watched Harbour Lights"},h):h==="time_remaining"?e.jsxs("div",{className:"metadata-preview-progress","data-colour":o,children:[e.jsx("span",{children:e.jsx("i",{})}),e.jsx("b",{children:"47 minutes remaining"})]},h):h==="summary"?e.jsx("p",{className:"metadata-preview-summary",children:"A quiet coastal town follows an unexpected signal across the winter sky."},h):null)}),e.jsx("div",{className:"metadata-hero-preview-art","aria-hidden":"true"})]})})]})]})}function Kt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(null),o=s==null?void 0:s.features,u=(o==null?void 0:o.features)??[],j=(o==null?void 0:o.configuration)??[],p=(s==null?void 0:s.clients)??[],g=(o==null?void 0:o.revision)??0,b=(x,$,k,R)=>r($,async()=>{await i(()=>q.post("/admin/api/features",{action:x,expectedRevision:g,overrides:R??{}}),k),c(null),await a()}),h=p.filter(x=>(x.capabilities??[]).includes("server_features_v1")).length,m=!!(o!=null&&o.safeMode),y=(x,$)=>({...Object.fromEntries(u.filter(k=>k.source==="override").map(k=>[k.key,k.enabled])),[x]:$}),v=(x,$)=>({...Object.fromEntries(j.map(k=>[k.key,k.value])),[x]:$});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Client configuration",intro:"One server-owned control plane for what Memby renders: flags, values, page composition and contextual discovery."}),e.jsx(P,{message:n}),t||!o?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"Thin client control plane",intro:"The gateway decides which sections, heroes and discovery rows are delivered. The TV remains a fast renderer of known components and safely ignores anything newer.",icon:"tv",children:e.jsxs("div",{className:"chips",children:[e.jsx("a",{className:"chip",href:"/admin/hero",children:"Hero sources and pinned overrides"}),e.jsx("a",{className:"chip",href:"/admin/recommendations",children:"For You and recommendation pools"}),e.jsx("a",{className:"chip",href:"/admin/engagement",children:"Contextual row engagement signals"}),e.jsx("a",{className:"chip",href:"/admin/clients",children:"Client versions and capabilities"})]})}),e.jsx(M,{title:"Control plane",intro:"Every optional feature has a safe default, an explicit override and a remote recovery path. Safe mode turns all of them off at once; sign-in, browsing and playback are never optional.",icon:"sliders",tone:"ok",actions:e.jsx(T,{variant:m?void 0:"danger",busy:l==="safe",onClick:()=>m?void b("leave-safe-mode","safe","Safe mode ended."):c({action:"safe-mode",title:"Enable safe mode?",body:"Every optional feature is disabled immediately on every television. Core sign-in, browsing and playback remain available.",label:"Enable safe mode"}),children:m?"Leave safe mode":"Enable safe mode"}),children:e.jsx(Cs,{tiles:[{label:"features active",value:`${u.filter(x=>x.enabled).length} / ${u.length}`},{label:"explicit overrides",value:w(u.filter(x=>x.source==="override").length)},{label:"televisions reporting the control plane",value:`${h} / ${p.length}`},{label:"published revision",value:`r${w(g)}`}]})}),e.jsx(M,{title:"Central configuration",intro:"Typed values use the same revisioned control plane as feature flags. Global values are safe defaults; user, device and experimental scopes are available to the client contract as features grow.",icon:"sliders",children:e.jsx("div",{className:"stack",children:j.map(x=>{const $=x.value;return e.jsxs("div",{className:"row",children:[e.jsxs("div",{className:"grow",children:[e.jsx("strong",{children:x.name}),e.jsxs("div",{className:"hint",children:[x.description," · ",x.scopes.join(", ")]}),e.jsx(oe,{children:x.key})]}),x.type==="boolean"?e.jsx(Z,{label:$?"On":"Off",checked:!!$,disabled:l===x.key,onChange:k=>void r(x.key,async()=>{await i(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:v(x.key,k),overrides:Object.fromEntries(u.filter(R=>R.source==="override").map(R=>[R.key,R.enabled]))}),"Configuration saved."),await a()})}):x.type==="enum"?e.jsx("select",{value:String($),disabled:l===x.key,onChange:k=>void r(x.key,async()=>{await i(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:v(x.key,k.target.value),overrides:Object.fromEntries(u.filter(R=>R.source==="override").map(R=>[R.key,R.enabled]))}),"Configuration saved."),await a()}),children:(x.options??[]).map(k=>e.jsx("option",{value:k,children:k},k))}):x.type==="json"?e.jsx("textarea",{rows:4,value:JSON.stringify($,null,2),disabled:l===x.key,"aria-label":x.name,onChange:k=>{try{const R=JSON.parse(k.target.value);r(x.key,async()=>{await i(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:v(x.key,R),overrides:Object.fromEntries(u.filter(I=>I.source==="override").map(I=>[I.key,I.enabled]))}),"Configuration saved."),await a()})}catch{}}}):e.jsx("input",{type:"number",value:Number($),min:x.min,max:x.max,disabled:l===x.key,onChange:k=>void r(x.key,async()=>{await i(()=>q.post("/admin/api/features",{action:"save",expectedRevision:g,values:v(x.key,Number(k.target.value)),overrides:Object.fromEntries(u.filter(R=>R.source==="override").map(R=>[R.key,R.enabled]))}),"Configuration saved."),await a()})})]},x.key)})})}),e.jsx(ue,{cols:"2",children:u.length===0?e.jsx(M,{title:"Nothing registered",icon:"sliders",children:e.jsx(ee,{children:"No server features are registered."})}):u.map(x=>e.jsxs(M,{title:x.name,intro:x.description,actions:e.jsx(N,{tone:x.enabled?"ok":void 0,children:x.enabled?"active":"off"}),footer:e.jsxs("span",{className:"hint",children:["↳ ",x.recovery]}),children:[e.jsx(Z,{label:x.enabled?"On":"Off",hint:"Changing this applies the feature policy to every compatible television.",checked:x.enabled,disabled:l===x.key,onChange:$=>c({action:"feature",key:x.key,enabled:$,title:`${$?"Turn on":"Turn off"} ${x.name}?`,body:`${$?"Enable":"Disable"} this feature for every compatible television. ${x.recovery}`,label:$?"Turn on":"Turn off"})}),e.jsxs("div",{className:"chips",children:[e.jsx(oe,{children:x.key}),e.jsxs(oe,{children:["protocol ",w(x.minimumProtocol),"+"]}),e.jsx(oe,{tone:x.compatible?"ok":"warn",children:x.compatible?"server compatible":"compatibility blocked"}),e.jsx(oe,{tone:"note",children:x.area})]})]},x.key))}),e.jsx(M,{children:e.jsxs("div",{className:"row",children:[e.jsx(T,{disabled:!o.canRollback,onClick:()=>c({action:"rollback",title:"Roll back one revision?",body:"The previous published feature revision is restored on every television.",label:"Roll back"}),children:"Roll back one revision"}),e.jsx(T,{onClick:()=>c({action:"reset",title:"Clear every override?",body:"All features return to their safe software defaults.",label:"Clear overrides"}),children:"Clear all overrides"}),e.jsx("span",{className:"spacer"}),m?e.jsx(N,{tone:"warn",children:"safe mode · optional features off"}):e.jsxs(N,{tone:"ok",children:["live · revision r",w(g)]})]})})]}),d?e.jsx(ke,{title:d.title,body:d.body,confirmLabel:d.label,destructive:d.action!=="rollback",busy:l===d.action,onConfirm:()=>void b(d.action==="feature"?"save":d.action,d.action==="feature"?d.key??"feature":d.action,`${d.label} done.`,d.action==="feature"&&d.key?y(d.key,!!d.enabled):void 0),onCancel:()=>c(null)}):null]})}function Gt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i,show:l}=te(),{busy:r,run:d}=se(),[c,o]=f.useState(!0),[u,j]=f.useState("6.5"),[p,g]=f.useState(!1);f.useEffect(()=>{var h,m;p||!s||(o(((h=s.playbackPolicy)==null?void 0:h.prerollEnabled)!==!1),j(String((((m=s.playbackPolicy)==null?void 0:m.prerollDurationMs)??6500)/1e3)))},[s,p]);const b=()=>d("save",async()=>{const h=Number(u);if(!Number.isFinite(h)||h<1||h>30){l("The preroll duration must be between 1 and 30 seconds.","bad");return}await i(()=>q.post("/admin/api/playback-policy",{prerollEnabled:c,prerollDurationMs:Math.round(h*1e3)}),"Playback policy saved."),g(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Playback",intro:"Presentation policy sent with every playback launch."}),e.jsx(P,{message:n}),t?e.jsx(B,{rows:1}):e.jsxs(M,{title:"Upcoming-show preroll",intro:"Sent with every playback launch. A change applies to the next title opened on every gateway-connected television; no app release is required.",icon:"play",tone:"info",actions:c?e.jsxs(N,{tone:"ok",children:["on · ",u,"s"]}):e.jsx(N,{children:"off"}),footer:e.jsx(T,{variant:"primary",busy:r==="save",onClick:()=>void b(),children:"Save playback policy"}),children:[e.jsx(Z,{label:"Show the preroll before a title starts",checked:c,onChange:h=>{o(h),g(!0)}}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Duration",hint:"Between 1 and 30 seconds. The stream is already playing behind it.",children:e.jsx("input",{type:"number",min:1,max:30,step:.5,value:u,onChange:h=>{j(h.target.value),g(!0)}})})})]})]})}function Zt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(null),[o,u]=f.useState(null),[j,p]=f.useState(!1),g=s==null?void 0:s.subtitles,b=g==null?void 0:g.stored;f.useEffect(()=>{d||!g||c({bazarr:g.bazarrEnabled,openSubtitles:g.openSubtitlesEnabled,key:"",clearKey:!1,username:g.openSubtitlesUsername??"",password:"",clearLogin:!1})},[g,d]);const h=x=>c($=>$&&{...$,...x}),m=()=>r("save",async()=>{d&&(await i(()=>q.post("/admin/api/subtitle-settings",{bazarrEnabled:d.bazarr,openSubtitlesEnabled:d.openSubtitles,openSubtitlesApiKey:d.key.trim(),clearOpenSubtitlesApiKey:d.clearKey,openSubtitlesUsername:d.username.trim(),openSubtitlesPassword:d.password,clearOpenSubtitlesLogin:d.clearLogin}),"Subtitle settings saved."),c(null),await a())}),y=()=>r("test",async()=>{u(null);const x=await i(()=>q.post("/admin/api/subtitle-test"));u((x==null?void 0:x.results)??[])}),v=()=>r("clear",async()=>{await i(()=>q.post("/admin/api/subtitle-settings",{action:"clear-stored"}),"Stored subtitles deleted."),p(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Subtitles",intro:"Which providers a viewer may fetch a missing subtitle from."}),e.jsx(P,{message:n}),t||!g||!d?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"offered on televisions",value:g.available?"yes":"no",small:!0,icon:"captions",tone:g.available?"ok":void 0},{label:"providers on",value:w((g.bazarrEnabled&&g.bazarrConfigured?1:0)+(g.openSubtitlesEnabled?1:0)),icon:"list",tone:"note"},{label:"subtitles held",value:w((b==null?void 0:b.count)??0),icon:"database",tone:"data"},{label:"last fetched",value:F(b==null?void 0:b.latest),small:!0,icon:"clock"}]}),e.jsxs(ue,{cols:"2",children:[e.jsx(M,{title:"Bazarr",intro:"Bazarr writes the subtitle file beside the media file, so Emby finds it and the track behaves like one that was always there. Its address is deployment configuration; this switch only decides whether viewers may use it.",icon:"wrench",tone:"data",actions:g.bazarrConfigured?d.bazarr?e.jsx(N,{tone:"ok",children:"on"}):e.jsx(N,{children:"off"}):e.jsx(N,{children:"not configured"}),footer:e.jsx("span",{className:"hint",children:g.bazarrConfigured?`Configured at ${g.bazarrUrl}`:"Set MEMBY_BAZARR_URL and MEMBY_BAZARR_API_KEY to use Bazarr."}),children:e.jsx(Z,{label:"Offer Bazarr in the player",hint:"Off leaves every subtitle it has already written in place.",checked:d.bazarr,disabled:!g.bazarrConfigured,onChange:x=>h({bazarr:x})})}),e.jsxs(M,{title:"OpenSubtitles",intro:"OpenSubtitles hands back a file rather than writing one, so Memby keeps what it fetches and serves it to the television itself. Titles are matched on their IMDb or TMDb id, which is exact — there is no guessing at a name.",icon:"captions",tone:"note",actions:g.openSubtitlesEnabled?e.jsx(N,{tone:g.openSubtitlesAccount?"ok":"warn",children:g.openSubtitlesAccount?"on · signed in":"on · anonymous"}):e.jsx(N,{children:g.openSubtitlesKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(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(D,{label:"API key",hint:"From your consumer at opensubtitles.com. Leave blank to keep the saved key.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:d.key,placeholder:g.openSubtitlesKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange: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(D,{label:"Account username",hint:"Optional, and the difference between a working feature and one that stops after a few files: without an account, downloads come out of the small anonymous allowance.",children:e.jsx("input",{type:"text",autoComplete:"off",value:d.username,placeholder:"Not signed in",onChange:x=>h({username:x.target.value})})}),e.jsx(D,{label:"Account password",hint:"Leave blank to keep the saved one.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:d.password,onChange: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(M,{children:[e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>void m(),children:"Save subtitle settings"}),e.jsx(T,{busy:l==="test",icon:"pulse",onClick:()=>void y(),children:"Test the providers"}),e.jsx("span",{className:"hint",children:g.featureEnabled?"A change applies to the next title opened; no app release is required.":"Downloading subtitles is switched off on the Features page, so nothing here is offered."})]}),o===null?null:o.length===0?e.jsx(ee,{children:"No provider is switched on, so there was nothing to ask."}):e.jsx("div",{className:"list",children:o.map(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(N,{tone:x.ok?"ok":"bad",children:x.ok?"reachable":"not reachable"})})]},x.provider))})]}),e.jsx(M,{title:"Subtitles Memby is holding",intro:"Only files fetched from a provider that cannot write beside the media file are kept here; they are served to televisions as ordinary tracks on every later playback. Emptying this is safe — each one can be fetched again, at the cost of the download allowance that fetched it.",icon:"database",tone:"data",actions:b!=null&&b.count?e.jsxs(N,{tone:"data",children:[w(b.count)," files · ",he(b.bytes)]}):e.jsx(N,{children:"nothing held"}),footer:e.jsx(T,{variant:"danger",disabled:!(b!=null&&b.count),onClick:()=>p(!0),children:"Delete every stored subtitle"}),children:e.jsx(e.Fragment,{})})]}),j?e.jsx(ke,{title:"Delete every stored subtitle?",body:"Each one can be fetched again, at the cost of the download allowance that fetched it. Subtitles Bazarr wrote beside the media are untouched — those belong to Emby.",confirmLabel:"Delete",destructive:!0,busy:l==="clear",onConfirm:()=>void v(),onCancel:()=>p(!1)}):null]})}function Yt(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(null),[o,u]=f.useState(!1),j=s==null?void 0:s.updatePolicy;f.useEffect(()=>{d||!j||c({version:j.latestVersion??"",url:j.downloadUrl??"",notes:j.notes??"",retireBelow:j.retireBelowVersion??"",required:!!j.minimumVersion&&j.minimumVersion===j.latestVersion,destructive:!!j.retireBelowVersion&&j.retireBelowVersion===j.latestVersion})},[j,d]);const p=m=>r(m?"save":"off",async()=>{d&&(await i(()=>q.post("/admin/api/update-policy",{enabled:m,latestVersion:d.version.trim(),downloadUrl:d.url.trim(),notes:d.notes.trim(),required:d.required,destructive:d.destructive,retireBelowVersion:d.retireBelow.trim()}),m?"Update policy saved.":"Update prompts turned off."),u(!1),c(null),await a())}),g=!!(j!=null&&j.minimumVersion)&&(j==null?void 0:j.minimumVersion)===(j==null?void 0:j.latestVersion),b=!!(j!=null&&j.retireBelowVersion)&&(j==null?void 0:j.retireBelowVersion)===(j==null?void 0:j.latestVersion),h=m=>c(y=>y&&{...y,...m});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"App updates",intro:"Publish an optional or a required client update."}),e.jsx(P,{message:n}),t||!d?e.jsx(B,{rows:1}):e.jsxs(M,{title:"Update policy",intro:"Televisions check on every launch. An optional update is a prompt the viewer can dismiss; a required one covers the home screen until they update, so it needs a download URL that actually works.",icon:"download",tone:"info",actions:j!=null&&j.enabled?e.jsxs(N,{tone:g?"warn":"ok",children:[b?"sign-out · ":g?"required · ":"optional · ",j.latestVersion]}):e.jsx(N,{children:"off"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>d.required?u(!0):void p(!0),children:"Save policy"}),e.jsx(T,{busy:l==="off",onClick:()=>void p(!1),children:"Turn prompts off"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Latest version",children:e.jsx("input",{type:"text",value:d.version,placeholder:"0.2.63",onChange:m=>h({version:m.target.value})})}),e.jsx(D,{label:"APK URL",children:e.jsx("input",{type:"text",value:d.url,placeholder:"https://nas/memby/memby-0.2.63.apk",onChange:m=>h({url:m.target.value})})})]}),e.jsx(D,{label:"What's new",hint:"Shown on the television above the update button.",children:e.jsx("input",{type:"text",value:d.notes,placeholder:"One line the viewer reads",onChange:m=>h({notes:m.target.value})})}),e.jsx(D,{label:"Sign out builds below",hint:"The destructive compatibility floor. Leave blank to keep every supported viewer signed in.",children:e.jsx("input",{type:"text",value:d.retireBelow,placeholder:"0.2.44",onChange:m=>h({retireBelow:m.target.value})})}),e.jsx(Z,{label:"Require this update",hint:"Blocks the home screen on every television below this version.",checked:d.required,onChange:m=>h({required:m})}),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:m=>h(m?{destructive:!0,required:!0,retireBelow:d.version.trim()}:{destructive:!1,retireBelow:d.retireBelow.trim()===d.version.trim()?"":d.retireBelow})})]}),o&&d?e.jsx(ke,{title:d.destructive?"Sign every older television out?":"Require this update?",body:d.destructive?"This deletes sessions on every older television and forces viewers to sign in again after updating.":"Required updates block the home screen on every television below this version until they update.",confirmLabel:"Publish",destructive:d.destructive,busy:l==="save",onConfirm:()=>void p(!0),onCancel:()=>u(!1)}):null]})}const Jt=[{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 Qt(s){const n=[...Jt];for(const t of[s.defaultIntervalSeconds,s.intervalSeconds])t>0&&!n.some(a=>a.value===t)&&n.push({value:t,label:Ne(t).replace(/^every /,"Every ")});return n.sort((t,a)=>t.value-a.value)}const Ge=s=>s.defaultIntervalSeconds>0&&s.intervalSeconds!==s.defaultIntervalSeconds;function us(s){return s==="failed"?"bad":s==="running"?"info":s==="skipped"?"warn":"ok"}function ls(s){if(s.running)return{state:"running",label:"Running",tone:"info",title:"Running right now"};if(!s.enabled)return{state:"disabled",label:"Disabled",tone:"warn",title:"Switched off — it will not run on its schedule"};const n=s.lastRun;return n?n.status==="failed"?{state:"failed",label:"Failed",tone:"bad",title:n.error||"The last run failed"}:n.status==="skipped"?{state:"skipped",label:"Skipped",tone:"warn",title:n.detail||"The last run declined to do anything"}:{state:"successful",label:"Successful",tone:"ok",title:"The last run finished cleanly"}:{state:"never",label:"Never run",tone:void 0,title:"This task has not run since the gateway started recording"}}function Xt(s){if(s.state==="running"||s.state==="successful")return"ok";if(s.state==="failed")return"bad";if(s.state==="disabled"||s.state==="skipped")return"warn"}const Gs={failed:0,running:1,skipped:2,disabled:3,never:4,successful:5};function ei(s,n){const t=Gs[ls(s).state]-Gs[ls(n).state];if(t!==0)return t;if(!!s.group!=!!n.group)return s.group?-1:1;const a=s.group.localeCompare(n.group);return a!==0?a:s.name.localeCompare(n.name)}const si=2e4,ni=3e3;function ti(){const{wrap:s}=te(),{busy:n,run:t}=se(),[a,i]=f.useState(!1),{data:l,error:r,loading:d,reload:c}=Y("/admin/api/tasks?limit=60",{pollMs:a?ni:si}),o=(l==null?void 0:l.tasks)??[],u=o.some(m=>m.running);u!==a&&i(u);const j=m=>t(m.id,async()=>{await s(()=>q.post(`/admin/api/tasks/${encodeURIComponent(m.id)}/run`),`${m.name} started.`),await c()}),p=o.filter(m=>{var y;return((y=m.lastRun)==null?void 0:y.status)==="failed"}).length,g=o.filter(Ge).length,b=o.filter(m=>!m.enabled).length,h=[...o].sort(ei);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(P,{message:r}),d?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Tasks",value:w(o.length),icon:"clock",tone:"info"},{label:"Running now",value:w(o.filter(m=>m.running).length),icon:"pulse",tone:u?"ok":void 0},{label:"Last run failed",value:w(p),icon:"alert",tone:p>0?"bad":void 0},{label:"Switched off",value:w(b),icon:"power",tone:b>0?"warn":void 0},{label:"Retimed",value:w(g),icon:"clock",tone:g>0?"note":void 0}]}),p>0?e.jsx(xe,{tone:"bad",children:"A failed task publishes an administrative event, so the failure is in the activity feed and wherever your integrations send it — you did not have to be looking at this page."}):null,e.jsx(M,{title:"Tasks",intro:"Anything wrong sorts to the top. Open a task to change its schedule, switch it off, or read its own run history.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Service"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Status"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:h.length===0?e.jsx(J,{columns:8,children:"This gateway has no scheduled tasks registered."}):h.map(m=>{const y=ls(m),v=m.lastRun,x=`/admin/tasks/${encodeURIComponent(m.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(G,{className:"table-row-link",to:x,children:m.name}),e.jsx("span",{className:"table-sub",children:m.description})]}),e.jsx("td",{className:"muted nowrap",children:m.group||"Other"}),e.jsxs("td",{className:"nowrap",children:[Ne(m.intervalSeconds),Ge(m)?e.jsxs("span",{className:"table-sub",children:["default ",Ne(m.defaultIntervalSeconds)]}):null]}),e.jsxs("td",{className:"nowrap muted",title:v?F(v.startedAt):void 0,children:[v?fe(v.startedAt):"never",v!=null&&v.error?e.jsx("span",{className:"table-sub",children:v.error}):null]}),e.jsx("td",{className:"num muted",children:v?ye(v.durationMs):"—"}),e.jsx("td",{className:"nowrap muted",children:m.enabled?m.running?"now":ds(m.nextRun):"—"}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":Xt(y)}),e.jsx(N,{tone:y.tone,children:e.jsx("span",{title:y.title,children:y.label})})]})}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx(T,{size:"sm",icon:"play",busy:n===m.id,disabled:m.running,title:`Run ${m.name} now`,onClick:()=>void j(m)}),e.jsx(G,{className:"table-row-link",to:x,children:"Details"})]})})]},m.id)})})]})})}),e.jsx(M,{title:"Recent runs",intro:"Every task together and in order, which is what shows two jobs interfering with each other.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Detail"})]})}),e.jsx("tbody",{children:((l==null?void 0:l.runs.length)??0)===0?e.jsx(J,{columns:6,children:"No task has run yet."}):l==null?void 0:l.runs.map(m=>{var y;return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(m.startedAt),children:fe(m.startedAt)}),e.jsx("td",{children:e.jsx(G,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(m.taskId)}`,children:((y=o.find(v=>v.id===m.taskId))==null?void 0:y.name)??m.taskId})}),e.jsx("td",{className:"muted",children:m.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(m.status),children:m.status})}),e.jsx("td",{className:"num muted",children:ye(m.durationMs)}),e.jsx("td",{className:"muted",children:m.error||m.detail||"—"})]},m.id)})})]})})})]})]})}const ii=2e4,ai=3e3,ri=100;function li(s){const n=s.filter(l=>l.status!=="running"),t=n.filter(l=>l.status==="failed").length,a=n.filter(l=>l.durationMs>0),i=a.length?a.reduce((l,r)=>l+r.durationMs,0)/a.length:0;return{runs:n.length,failed:t,averageMs:i,timed:a.length}}function oi(){var y;const{taskId:s=""}=Be(),{wrap:n}=te(),{busy:t,run:a}=se(),[i,l]=f.useState(!1),{data:r,error:d,loading:c,reload:o}=Y(`/admin/api/tasks?task=${encodeURIComponent(s)}&limit=${ri}`,{pollMs:i?ai:ii,enabled:!!s}),u=r==null?void 0:r.tasks.find(v=>v.id===s),j=!!(u!=null&&u.running);j!==i&&l(j);const p=(r==null?void 0:r.runs)??[],g=u?ls(u):void 0,b=li(p),h=(v,x,$)=>a(v,async()=>{await n(()=>q.put(`/admin/api/tasks/${encodeURIComponent(s)}`,$),x),await o()}),m=v=>a("run",async()=>{await n(()=>q.post(`/admin/api/tasks/${encodeURIComponent(s)}/run`),`${v.name} started.`),await o()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:(u==null?void 0:u.name)||s,intro:u==null?void 0:u.description,icon:"clock",crumbs:e.jsxs(e.Fragment,{children:[e.jsx(G,{to:"/admin/tasks",children:"Scheduled tasks"}),e.jsx("span",{children:"/"}),e.jsx("span",{children:(u==null?void 0:u.name)||s})]}),actions:u?e.jsx(T,{variant:"primary",icon:"play",busy:t==="run",disabled:u.running,onClick:()=>void m(u),children:u.running?"Running":"Run now"}):void 0}),e.jsx(P,{message:d}),c?e.jsx(B,{}):u?e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Status",value:(g==null?void 0:g.label)??"—",small:!0,icon:"pulse",tone:g==null?void 0:g.tone},{label:"Service",value:u.group||"Other",small:!0,icon:"chip",tone:"info"},{label:"Runs every",value:Ne(u.intervalSeconds),small:!0,icon:"clock",tone:Ge(u)?"note":void 0},{label:"Next run",value:u.enabled?u.running?"now":ds(u.nextRun):"not scheduled",small:!0,icon:"history",tone:u.enabled?void 0:"warn"},{label:`Failed of the last ${w(b.runs)}`,value:w(b.failed),icon:"alert",tone:b.failed>0?"bad":void 0},{label:`Average of ${w(b.timed)} timed runs`,value:b.timed?ye(Math.round(b.averageMs)):"—",small:!0,icon:"chart",tone:"data"}]}),(y=u.lastRun)!=null&&y.error?e.jsxs(xe,{tone:"bad",children:["The last run failed: ",u.lastRun.error]}):null,e.jsxs(M,{title:"Schedule",intro:"How often the gateway runs this on its own, and whether it runs it at all.",icon:"sliders",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx(D,{label:"How often it runs",children:e.jsx("select",{"aria-label":`How often ${u.name} runs`,value:u.intervalSeconds,disabled:t==="interval"||u.running,onChange:v=>void h("interval",`${u.name} now runs ${Ne(Number(v.target.value))}.`,{intervalSeconds:Number(v.target.value)}),children:Qt(u).map(v=>e.jsxs("option",{value:v.value,children:[v.label,v.value===u.defaultIntervalSeconds?" (default)":""]},v.value))})}),Ge(u)?e.jsxs(T,{icon:"refresh",busy:t==="interval",disabled:u.running,onClick:()=>void h("interval",`${u.name} back to its default cadence.`,{intervalSeconds:0}),children:["Back to ",Ne(u.defaultIntervalSeconds)]}):null]}),e.jsx(Z,{label:"Run this on its schedule",hint:"Switched off, the gateway leaves it alone. You can still start it by hand.",checked:u.enabled,disabled:t==="enabled",onChange:v=>void h("enabled",v?`${u.name} switched on.`:`${u.name} switched off.`,{enabled:v})}),Ge(u)?e.jsxs(xe,{tone:"note",children:["This task is retimed: its code asks for ",Ne(u.defaultIntervalSeconds)," and it is set to ",Ne(u.intervalSeconds),"."]}):null]}),e.jsx(M,{title:"Run history",intro:"This task alone, newest first — which is what separates a job that fails occasionally from one that has stopped working.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"}),e.jsx("th",{children:"Detail"})]})}),e.jsx("tbody",{children:p.length===0?e.jsx(J,{columns:5,children:"This task has not run yet."}):p.map(v=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(v.startedAt),children:fe(v.startedAt)}),e.jsx("td",{className:"muted",children:v.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(v.status),children:v.status})}),e.jsx("td",{className:"num muted",children:ye(v.durationMs)}),e.jsx("td",{className:"muted",children:v.error||v.detail||"—"})]},v.id))})]})})})]}):e.jsx(M,{title:"No such task",icon:"alert",tone:"bad",children:e.jsxs(ee,{children:["This gateway has no task called ",e.jsx("code",{children:s}),". It may have been renamed or removed."," ",e.jsx(G,{to:"/admin/tasks",children:"Back to scheduled tasks"}),"."]})})]})}function Pe(s){switch(s){case"error":return"bad";case"healthy":return"ok";case"running":return"info";case"disabled":return"warn";default:return}}function In(s){return Pe(s)}const Zs={error:0,running:1,idle:2,healthy:3,disabled:4,unconfigured:5};function ci(s,n){const t=Zs[s.status]-Zs[n.status];return t!==0?t:s.name.localeCompare(n.name)}const di=s=>!!(s&&(s.processed||s.changed||s.skipped||s.failed));function Je(s){if(!di(s)||!s)return"";const n=[];return s.processed&&n.push(`${s.processed.toLocaleString()} checked`),s.changed&&n.push(`${s.changed.toLocaleString()} changed`),s.skipped&&n.push(`${s.skipped.toLocaleString()} skipped`),s.failed&&n.push(`${s.failed.toLocaleString()} failed`),n.join(" · ")}function Ms(s){if(s.status==="running")return"Running…";if(s.error)return s.error;if(s.detail)return s.detail;const n=Je(s.counts);return n||"No changes"}function hi(s){return s.powers.length?`Switching ${s.name} off also stops: ${s.powers.join("; ")}.`:""}const ui=2e4,mi=3e3;function pi(){const[s,n]=f.useState(!1),{data:t,error:a,loading:i}=Y("/admin/api/integrations/services?limit=40",{pollMs:s?mi:ui}),l=((t==null?void 0:t.services)??[]).some(p=>p.running);l!==s&&n(l);const r=[...(t==null?void 0:t.services)??[]].sort(ci),d=(t==null?void 0:t.runs)??[],c=r.filter(p=>p.status==="error"),o=r.filter(p=>p.running),u=r.filter(p=>p.configured&&!p.enabled),j=r.filter(p=>!p.configured);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Integrations",intro:"The services Memby depends on: whether each one is configured, whether it is working, what it is doing right now and what it last managed to do."}),e.jsx(P,{message:a}),i?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"Integrations",value:w(r.length),icon:"plug",tone:"info"},{label:"Not working",value:w(c.length),icon:"alert",tone:c.length>0?"bad":void 0},{label:"Running now",value:w(o.length),icon:"pulse",tone:o.length>0?"ok":void 0},{label:"Switched off",value:w(u.length),icon:"power",tone:u.length>0?"warn":void 0},{label:"Not configured",value:w(j.length),icon:"sliders",tone:j.length>0?"note":void 0}]}),c.length>0?e.jsxs(xe,{tone:"bad",children:[c.map(p=>p.name).join(", ")," ",c.length===1?"is not answering":"are not answering",". A failed run also publishes an administrative event, so it is in the activity feed and wherever your webhooks send it — you did not have to be looking at this page."]}):null,e.jsx(M,{title:"Services",intro:"Anything wrong sorts to the top. Open a service to switch it off, read its settings, or see everything it has done.",icon:"plug",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Integration"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{children:"Enabled"})]})}),e.jsx("tbody",{children:r.length===0?e.jsx(J,{columns:6,children:"This gateway has no integrations."}):r.map(p=>e.jsx(xi,{service:p},p.id))})]})})}),e.jsx(M,{title:"Recent activity",intro:"Every integration's work together and in order, which is what shows two of them getting in each other's way. This is what Memby attempted and what came of it — the Logs page is where the technical detail behind a failure lives.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Integration"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:d.length===0?e.jsx(J,{columns:6,children:"No integration has run yet."}):d.map(p=>{const g=r.find(h=>h.id===p.integrationId),b=g==null?void 0:g.tasks.find(h=>h.id===p.taskId);return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(p.startedAt),children:fe(p.startedAt)}),e.jsx("td",{className:"nowrap",children:e.jsx(G,{className:"table-row-link",to:`/admin/integrations/${encodeURIComponent(p.integrationId??"")}`,children:(g==null?void 0:g.name)??p.integrationId})}),e.jsx("td",{className:"muted",children:(b==null?void 0:b.name)??p.taskId}),e.jsx("td",{children:e.jsx(N,{tone:us(p.status),children:p.status})}),e.jsxs("td",{className:"muted",children:[Ms(p),p.error&&Je(p.counts)?e.jsx("span",{className:"table-sub",children:Je(p.counts)}):null]}),e.jsx("td",{className:"num muted",children:ye(p.durationMs)})]},p.id)})})]})})})]})]})}function xi({service:s}){const n=`/admin/integrations/${encodeURIComponent(s.id)}`;return e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(G,{className:"table-row-link",to:n,children:s.name}),e.jsx("span",{className:"table-sub",children:s.address||s.summary})]}),e.jsx("td",{className:"nowrap",children:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":In(s.status)}),e.jsx(N,{tone:Pe(s.status),children:e.jsx("span",{title:s.detail||s.statusLabel,children:s.statusLabel})})]})}),e.jsx("td",{className:"nowrap muted",title:s.lastRun?F(s.lastRun.startedAt):void 0,children:s.lastRun?fe(s.lastRun.startedAt):"never"}),e.jsx("td",{className:"muted",children:s.lastRun?Ms(s.lastRun):s.detail||"—"}),e.jsx("td",{className:"nowrap muted",children:s.configured?s.enabled?s.running?"now":ds(s.nextRun):"off":"—"}),e.jsx("td",{className:"nowrap",children:s.configured?s.enabled?e.jsx(N,{tone:"ok",children:"On"}):e.jsx(N,{tone:"warn",children:"Off"}):e.jsx(N,{children:"not configured"})})]})}const ji={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 gi(){const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(!1),[o,u]=f.useState(""),[j,p]=f.useState(!1),[g,b]=f.useState([]),[h,m]=f.useState(!1),y=s==null?void 0:s.mdblist;f.useEffect(()=>{h||!y||(c(y.enabled),b(y.sources??[]))},[y,h]);const v=()=>r("save",async()=>{await i(()=>q.post("/admin/api/mdblist-settings",{enabled:d,apiKey:o.trim(),clearApiKey:j,sources:g}),"Ratings settings saved."),u(""),p(!1),m(!1),await a()}),x=(y==null?void 0:y.cachedTitles)??0;return e.jsxs(e.Fragment,{children:[e.jsx(P,{message:n}),t||!y?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsxs(ue,{cols:"2",children:[e.jsxs(M,{title:"MDBList connection",intro:"The key stays on this server and a failure never blocks a television. Every rating fetched is stored here permanently and re-checked about once a month, so browsing the library costs nothing after the first look at a title.",icon:"star",tone:"note",actions:d?e.jsxs(N,{tone:"ok",children:["on · ",g.length," sources"]}):e.jsx(N,{children:y.apiKeyConfigured?"off · key saved":"off · no key"}),children:[e.jsx(Z,{label:"Show external ratings on televisions",hint:"Off leaves the stored ratings in place.",checked:d,onChange:$=>{c($),m(!0)}}),e.jsx(D,{label:"API key",hint:"Leave blank to keep the key that is already saved.",children:e.jsx("input",{type:"password",autoComplete:"new-password",value:o,placeholder:y.apiKeyConfigured?"Saved key (leave blank to keep)":"Paste an API key",onChange:$=>u($.target.value)})}),e.jsx(Z,{label:"Remove the saved key",checked:j,onChange:p})]}),e.jsx(M,{title:"Sources shown on televisions",intro:"A title with none of these has no ratings strip at all, which is the honest answer — nothing stands in for a score that was never fetched.",icon:"list",tone:"data",children:(y.availableSources??[]).length===0?e.jsx(ee,{children:"No rating sources are available."}):e.jsx("div",{className:"checks columns",children:(y.availableSources??[]).map($=>e.jsx(Z,{label:ji[$]??$,checked:g.includes($),onChange:k=>{m(!0),b(R=>k?[...R,$]:R.filter(I=>I!==$))}},$))})})]}),e.jsx(M,{children:e.jsxs("div",{className:"row",children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>void v(),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."})]})})]})]})}const vi={id:"",name:"Discord",url:"",enabled:!0,events:[]};function fi(){const{wrap:s,show:n}=te(),{busy:t,run:a}=se(),{data:i,error:l,loading:r,reload:d}=Y("/admin/api/integrations",{pollMs:6e4}),[c,o]=f.useState(null),[u,j]=f.useState(null),p=(i==null?void 0:i.catalogue)??[],g=(i==null?void 0:i.integrations)??[],b=v=>o({id:v.id,name:v.name,url:"",enabled:v.enabled,events:v.events??[]}),h=()=>a("save",async()=>{if(!c)return;await s(()=>q.post("/admin/api/integrations",c),c.id?"Integration saved.":"Integration added.")&&(o(null),await d())}),m=v=>a("remove",async()=>{await s(()=>q.del(`/admin/api/integrations/${encodeURIComponent(v.id)}`),`${v.name} removed.`),j(null),await d()}),y=v=>a(`test:${v.id}`,async()=>{const x=await s(()=>q.post(`/admin/api/integrations/${encodeURIComponent(v.id)}/test`));x&&n(x.message,x.ok?"ok":"bad"),await d()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Event webhooks",intro:"Send administrative events to somewhere you already look. Events pass through the gateway's own event layer, so nothing about authentication or scheduled tasks knows Discord exists — and a second kind of destination is a change here rather than everywhere.",actions:e.jsx(T,{variant:"primary",icon:"plus",onClick:()=>o(vi),children:"Add a webhook"})}),e.jsx(P,{message:l}),((i==null?void 0:i.dropped)??0)>0?e.jsxs(xe,{tone:"warn",children:[w((i==null?void 0:i.dropped)??0)," events could not be queued for delivery. The queue is deliberately lossy — a slow endpoint must never hold up a television signing in — but a number growing here means a destination is not keeping up."]}):null,r?e.jsx(B,{}):g.length===0&&!c?e.jsx(M,{title:"Nothing configured",icon:"plug",tone:"note",children:e.jsx(ee,{children:"No destinations yet. A Discord webhook takes about a minute: in Discord, open a channel's settings → Integrations → Webhooks → New Webhook, copy its URL, and paste it here."})}):g.map(v=>e.jsx(wi,{integration:v,catalogue:p,busy:t,onEdit:()=>b(v),onTest:()=>void y(v),onRemove:()=>j(v)},v.id)),c?e.jsx(ki,{draft:c,catalogue:p,busy:t==="save",onChange:o,onSave:()=>void h(),onCancel:()=>o(null)}):null,u?e.jsx(ke,{title:`Remove ${u.name}?`,body:"The webhook address and its delivery history go with it. Events already published stay in the activity feed.",confirmLabel:"Remove",destructive:!0,busy:t==="remove",onConfirm:()=>void m(u),onCancel:()=>j(null)}):null]})}function bi(){const{wrap:s}=te(),{busy:n,run:t}=se(),{data:a,error:i,loading:l,reload:r}=Y("/admin/api/sonarr-request-policy"),[d,c]=f.useState(0),[o,u]=f.useState(!1);f.useEffect(()=>{a&&(c(a.qualityProfileId),u(a.searchImmediately))},[a]);const j=()=>t("sonarr-request-policy",async()=>{await s(()=>q.post("/admin/api/sonarr-request-policy",{qualityProfileId:d,searchImmediately:o}),"Sonarr TV request policy saved."),await r()}),p=a==null?void 0:a.profiles.find(g=>g.id===d);return e.jsxs(M,{title:"Sonarr TV requests",intro:"The policy Memby uses when a viewer requests a television series. The series remains monitored; searching its existing episodes is an explicit choice.",icon:"tv",tone:a!=null&&a.configured?"ok":"warn",actions:a!=null&&a.configured?e.jsx(N,{tone:"ok",children:"configured"}):e.jsx(N,{tone:"warn",children:"needs attention"}),footer:e.jsx(T,{variant:"primary",busy:n==="sonarr-request-policy",disabled:l||d<=0,onClick:()=>void j(),children:"Save Sonarr policy"}),children:[e.jsx(P,{message:i??(a==null?void 0:a.error)??""}),l?e.jsx(B,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Request quality profile",hint:"Memby stores this Sonarr profile ID. 720p is the recommended safe default; Memby will never fall back to Any.",children:e.jsxs("select",{value:d,onChange:g=>c(Number(g.target.value)),disabled:!(a!=null&&a.profiles.length),children:[e.jsx("option",{value:0,children:"Choose a quality profile…"}),a==null?void 0:a.profiles.map(g=>e.jsxs("option",{value:g.id,children:[g.name,g.recommended?" — recommended (720p)":""]},g.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:o,onChange:u}),p?e.jsxs(xe,{tone:"info",children:["Requested series will use ",e.jsx("b",{children:p.name})," (profile ID ",p.id,"), be monitored using Memby’s existing all-episodes strategy, and ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function yi(){const{wrap:s}=te(),{busy:n,run:t}=se(),{data:a,error:i,loading:l,reload:r}=Y("/admin/api/radarr-request-policy"),[d,c]=f.useState(0),[o,u]=f.useState(!1);f.useEffect(()=>{a&&(c(a.qualityProfileId),u(a.searchImmediately))},[a]);const j=()=>t("radarr-request-policy",async()=>{await s(()=>q.post("/admin/api/radarr-request-policy",{qualityProfileId:d,searchImmediately:o}),"Radarr movie request policy saved."),await r()}),p=a==null?void 0:a.profiles.find(g=>g.id===d);return e.jsxs(M,{title:"Radarr movie requests",intro:"The policy Memby uses when a viewer requests a film. The film remains monitored; an immediate Radarr search is an explicit choice.",icon:"tv",tone:a!=null&&a.configured?"ok":"warn",actions:a!=null&&a.configured?e.jsx(N,{tone:"ok",children:"configured"}):e.jsx(N,{tone:"warn",children:"needs attention"}),footer:e.jsx(T,{variant:"primary",busy:n==="radarr-request-policy",disabled:l||d<=0,onClick:()=>void j(),children:"Save Radarr policy"}),children:[e.jsx(P,{message:i??(a==null?void 0:a.error)??""}),l?e.jsx(B,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Request quality profile",hint:"Memby stores this Radarr profile ID. 720p is the recommended safe default; Memby will never fall back to Any.",children:e.jsxs("select",{value:d,onChange:g=>c(Number(g.target.value)),disabled:!(a!=null&&a.profiles.length),children:[e.jsx("option",{value:0,children:"Choose a quality profile…"}),a==null?void 0:a.profiles.map(g=>e.jsxs("option",{value:g.id,children:[g.name,g.recommended?" — recommended (720p)":""]},g.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:o,onChange:u}),p?e.jsxs(xe,{tone:"info",children:["Requested films will use ",e.jsx("b",{children:p.name})," (profile ID ",p.id,"), remain monitored, and ",o?"start an immediate search.":"not start an immediate search."]}):null]})]})}function wi({integration:s,catalogue:n,busy:t,onEdit:a,onTest:i,onRemove:l}){const r=s.health,d=!r.lastFailure||r.lastSuccess&&r.lastSuccess>r.lastFailure,c=s.events??[];return e.jsxs(M,{title:s.name,intro:s.hint?`Discord webhook ${s.hint}`:"Discord webhook",icon:"plug",tone:s.enabled?"ok":"warn",actions:e.jsxs(e.Fragment,{children:[s.enabled?e.jsx(N,{tone:"ok",children:"on"}):e.jsx(N,{tone:"warn",children:"off"}),r.deliveries>0?e.jsx(N,{tone:d?"ok":"bad",children:d?"delivering":"failing"}):e.jsx(N,{children:"never used"}),e.jsx(T,{size:"sm",icon:"pulse",busy:t===`test:${s.id}`,onClick:i,children:"Test"}),e.jsx(T,{size:"sm",onClick:a,children:"Edit"}),e.jsx(T,{size:"sm",variant:"danger",icon:"trash",onClick:l,title:"Remove"})]}),children:[e.jsxs("div",{className:"list",children:[e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Events sent"}),e.jsx("p",{children:c.length===0?"None selected — this destination is configured but will never post anything.":c.map(o=>{var u;return((u=n.find(j=>j.type===o))==null?void 0:u.label)??o}).join(", ")})]})}),e.jsxs("div",{className:"list-item",children:[e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last delivered"}),e.jsx("p",{children:r.lastSuccess?F(r.lastSuccess):"never"})]}),e.jsx("div",{className:"list-actions",children:r.deliveries>0?e.jsxs("span",{className:"quiet",children:[w(r.deliveries)," attempts, ",w(r.failures)," failed"]}):null})]}),r.lastFailure?e.jsx("div",{className:"list-item",children:e.jsxs("div",{className:"list-body",children:[e.jsx("b",{children:"Last failure"}),e.jsxs("p",{children:[F(r.lastFailure),r.lastError?` — ${r.lastError}`:""]})]})}):null]}),s.deliveries.length>0?e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Attempted"}),e.jsx("th",{children:"Event"}),e.jsx("th",{children:"Result"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:s.deliveries.map(o=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(o.attemptedAt),children:fe(o.attemptedAt)}),e.jsx("td",{className:"muted",children:o.eventType}),e.jsx("td",{children:o.success?e.jsx(N,{tone:"ok",children:o.statusCode||"ok"}):e.jsx(N,{tone:"bad",children:o.error||o.statusCode||"failed"})}),e.jsx("td",{className:"num muted",children:ye(o.durationMs)})]},o.id))})]})}):e.jsx(K,{children:e.jsx("table",{children:e.jsx("tbody",{children:e.jsx(J,{columns:4,children:"Nothing has been delivered through this webhook yet."})})})})]})}function ki({draft:s,catalogue:n,busy:t,onChange:a,onSave:i,onCancel:l}){const r=[...new Set(n.map(c=>c.group))],d=(c,o)=>a({...s,events:o?[...s.events,c]:s.events.filter(u=>u!==c)});return e.jsxs(M,{title:s.id?`Edit ${s.name}`:"New Discord webhook",icon:"plug",tone:"info",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:t,onClick:i,children:s.id?"Save":"Add"}),e.jsx(T,{variant:"quiet",onClick:l,children:"Cancel"}),e.jsx("span",{className:"spacer"}),s.events.length===0?e.jsx("span",{className:"quiet",children:"Nothing selected — this destination would never post."}):e.jsxs("span",{className:"quiet",children:[s.events.length," events selected"]})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Name",hint:"What this destination is called in the console.",children:e.jsx("input",{type:"text",value:s.name,onChange:c=>a({...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=>a({...s,url:c.target.value})})})]}),e.jsx(Z,{label:"Enabled",hint:"Off keeps the configuration and stops the posts.",checked:s.enabled,onChange:c=>a({...s,enabled:c})}),r.map(c=>e.jsxs("div",{children:[e.jsx("div",{className:"card-head",style:void 0,children:e.jsx("div",{className:"card-head-text",children:e.jsx("h2",{children:c})})}),n.filter(o=>o.group===c).map(o=>e.jsx(Z,{label:o.label,hint:o.description,checked:s.events.includes(o.type),onChange:u=>d(o.type,u)},o.type))]},c))]})}const Ni=2e4,Si=3e3;function Ci(){const{integrationId:s=""}=Be(),{wrap:n,show:t}=te(),{busy:a,run:i}=se(),[l,r]=f.useState(!1),{data:d,error:c,loading:o,reload:u}=Y(`/admin/api/integrations/services/${encodeURIComponent(s)}?limit=80`,{pollMs:l?Si:Ni,enabled:!!s}),j=d==null?void 0:d.service,p=(d==null?void 0:d.runs)??[],g=!!(j!=null&&j.running);g!==l&&r(g);const b=v=>i("enabled",async()=>{await n(()=>q.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/enabled`,{enabled:v}),`${(j==null?void 0:j.name)??"Integration"} ${v?"enabled":"disabled"}.`),await u()}),h=()=>i("test",async()=>{const v=await n(()=>q.post(`/admin/api/integrations/services/${encodeURIComponent(s)}/test`));v&&t(v.reachable?`${j==null?void 0:j.name} answered in ${ye(v.latencyMs)}.`:v.error||`${j==null?void 0:j.name} did not answer.`,v.reachable?"ok":"bad"),await u()}),m=(v,x)=>i(v,async()=>{await n(()=>q.post(`/admin/api/tasks/${encodeURIComponent(v)}/run`),`${x} started.`),await u()});if(o||!j)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Integration",intro:"One external service and everything it has done."}),e.jsx(P,{message:c}),o?e.jsx(B,{}):e.jsx(xe,{tone:"warn",children:"No such integration."})]});const y=p.filter(v=>v.status==="failed").length;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:j.name,intro:j.summary,actions:e.jsx(G,{className:"table-row-link",to:"/admin/integrations",children:"All integrations"})}),e.jsx(P,{message:c}),e.jsx(ae,{tiles:[{label:"Status",value:j.statusLabel,small:!0,icon:"pulse",tone:Pe(j.status)},{label:"Runs recorded",value:w(j.runs),icon:"history",tone:"data"},{label:"Failed runs",value:w(j.failures),icon:"alert",tone:j.failures>0?"bad":void 0},{label:"Last worked",value:j.lastSuccessAt?fe(j.lastSuccessAt):"never",small:!0,icon:"check",tone:j.lastSuccessAt?"ok":void 0}]}),e.jsxs(M,{title:"Connection",intro:"Address and credentials come from this gateway's environment; the switch is stored on the server and applies to the whole household.",icon:"plug",tone:Pe(j.status)??"info",actions:e.jsxs("span",{className:"row tight",children:[e.jsx("span",{className:"dot-state","data-tone":In(j.status)}),e.jsx(N,{tone:Pe(j.status),children:j.statusLabel})]}),footer:j.configured&&j.probed?e.jsx(T,{icon:"sync",busy:a==="test",onClick:()=>void h(),children:"Test connection"}):void 0,children:[j.detail?e.jsx(xe,{tone:Pe(j.status),children:j.detail}):null,j.configured?e.jsxs(e.Fragment,{children:[e.jsx(Z,{label:`${j.name} enabled`,hint:j.enabled?hi(j):`Memby is not calling ${j.name} or scheduling any of its work.`,checked:j.enabled,disabled:a==="enabled",onChange:v=>void b(v)}),e.jsx(We,{rows:[{label:"Address",value:j.address||"—"},...(j.facts??[]).map(v=>({label:v.label,value:v.tone?e.jsx(N,{tone:v.tone==="warn"?"warn":"ok",children:v.value}):v.value})),{label:"Last checked",value:j.probed?j.health?`${fe(j.health.checkedAt)} · ${ye(j.health.latencyMs)}`:"not yet":"Not probed — see below"}]}),j.probed?null:e.jsxs(xe,{children:[j.name," is not probed for reachability: its allowance is bought by the day, and spending a request of it to draw a status would compete with the televisions for the thing being reported on. Its health comes from the run history below."]})]}):e.jsxs(xe,{tone:"warn",children:["This gateway has no address or credential for ",j.name,". Set them in the deployment's environment and restart the container; there is nothing to switch until then."]})]}),j.powers.length>0?e.jsx(M,{title:"What depends on this",intro:"Switching the service off stops all of it. Nothing below fails quietly — it stops being offered.",icon:"journey",tone:"note",children:e.jsx("div",{className:"list",children:j.powers.map(v=>e.jsxs("div",{className:"list-item",children:[e.jsx(Qe,{name:"check",tone:"ok"}),e.jsx("span",{className:"list-body",children:v})]},v))})}):null,e.jsx(Ti,{id:j.id}),e.jsx(M,{title:"Scheduled work",intro:"The background jobs belonging to this service. Cadence and the per-job switch live on each job's own page; this is where you start one by hand.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Schedule"}),e.jsx("th",{children:"Last run"}),e.jsx("th",{children:"Next run"}),e.jsx("th",{})]})}),e.jsx("tbody",{children:j.tasks.length===0?e.jsx(J,{columns:5,children:"This service has no scheduled work: Memby calls it when a television asks for something rather than on a timer."}):j.tasks.map(v=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx(G,{className:"table-row-link",to:`/admin/tasks/${encodeURIComponent(v.id)}`,children:v.name}),e.jsx("span",{className:"table-sub",children:v.description})]}),e.jsx("td",{className:"nowrap muted",children:Ne(v.intervalSeconds)}),e.jsx("td",{className:"nowrap muted",title:v.lastRun?F(v.lastRun.startedAt):void 0,children:v.lastRun?fe(v.lastRun.startedAt):"never"}),e.jsx("td",{className:"nowrap muted",children:!v.enabled||!j.enabled?"off":v.running?"now":ds(v.nextRun)}),e.jsx("td",{className:"nowrap",children:e.jsx(T,{size:"sm",icon:"play",busy:a===v.id,disabled:v.running,title:`Run ${v.name} now`,onClick:()=>void m(v.id,v.name)})})]},v.id))})]})})}),e.jsx(M,{title:"Run history",intro:"What Memby attempted on this service's behalf and what came of it. This is not the application log — a failure here gives the reason, and Logs is where the technical detail behind it lives.",icon:"history",tone:"note",actions:y>0?e.jsx(G,{className:"table-row-link",to:`/admin/logs?q=${encodeURIComponent(j.id)}`,children:"Open the logs"}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"Started"}),e.jsx("th",{children:"Task"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Checked"}),e.jsx("th",{className:"num",children:"Changed"}),e.jsx("th",{className:"num",children:"Skipped"}),e.jsx("th",{className:"num",children:"Failed"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:p.length===0?e.jsxs(J,{columns:10,children:["Nothing has run for ",j.name," yet."]}):p.map(v=>e.jsx(Mi,{run:v,service:j},v.id))})]})})})]})}function Mi({run:s,service:n}){var i,l,r,d;const t=n.tasks.find(c=>c.id===s.taskId),a=c=>c?w(c):"—";return e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(s.startedAt),children:fe(s.startedAt)}),e.jsx("td",{className:"muted",children:(t==null?void 0:t.name)??s.taskId}),e.jsx("td",{className:"muted",children:s.trigger}),e.jsx("td",{children:e.jsx(N,{tone:us(s.status),children:s.status})}),e.jsxs("td",{className:"muted",children:[Ms(s),s.error&&Je(s.counts)?e.jsx("span",{className:"table-sub",children:Je(s.counts)}):null]}),e.jsx("td",{className:"num muted",children:a(((i=s.counts)==null?void 0:i.processed)??0)}),e.jsx("td",{className:"num muted",children:a(((l=s.counts)==null?void 0:l.changed)??0)}),e.jsx("td",{className:"num muted",children:a(((r=s.counts)==null?void 0:r.skipped)??0)}),e.jsx("td",{className:"num muted",children:a(((d=s.counts)==null?void 0:d.failed)??0)}),e.jsx("td",{className:"num muted",children:ye(s.durationMs)})]})}function Ti({id:s}){switch(s){case"mdblist":return e.jsx(gi,{});case"sonarr":return e.jsx(bi,{});case"radarr":return e.jsx(yi,{});default:return null}}function $i(){var X,le,me,ie;const{status:s,error:n,loading:t,reload:a}=je(),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(""),[o,u]=f.useState(!1),[j,p]=f.useState(!1),[g,b]=f.useState(!1),[h,m]=f.useState("23:00"),[y,v]=f.useState("07:00"),[x,$]=f.useState(""),[k,R]=f.useState(!1),I=!!((X=s==null?void 0:s.maintenance)!=null&&X.enabled);f.useEffect(()=>{var H;!j&&s&&c(((H=s.maintenance)==null?void 0:H.message)??"")},[s,j]),f.useEffect(()=>{k||!(s!=null&&s.quietTime)||(b(s.quietTime.enabled),m(s.quietTime.startTime),v(s.quietTime.endTime),$(s.quietTime.message))},[s,k]);const A=H=>r(H?"on":"off",async()=>{await i(()=>q.post("/admin/api/maintenance",{enabled:H,message:d}),H?"Memby is offline for every television.":"Memby is back online."),u(!1),p(!1),await a()}),L=()=>r("quiet",async()=>{await i(()=>q.post("/admin/api/quiet-time",{enabled:g,startTime:h,endTime:y,message:x}),g?"Quiet time saved.":"Quiet time turned off."),R(!1),await a()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Maintenance",intro:"Take Memby offline now or schedule daily quiet time."}),e.jsx(P,{message:n}),t?e.jsx(B,{rows:1}):e.jsx(M,{title:"Gateway availability",intro:"Takes Memby offline for every television, independently of Emby. Sign-in and all content calls answer 503 with the message below, and the television shows it in place of the launcher rows. This console keeps working.",icon:"power",tone:I?"bad":"warn",actions:I?e.jsx(N,{tone:"bad",children:"offline"}):e.jsx(N,{tone:"ok",children:"online"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"danger",disabled:I,onClick:()=>u(!0),children:"Go offline"}),e.jsx(T,{disabled:!I,busy:l==="off",onClick:()=>void A(!1),children:"Bring back online"})]}),children:e.jsx(D,{label:"Message shown on the television",hint:"Say what is happening and when it will be back. It is the only thing the viewer is told.",children:e.jsx("input",{type:"text",value:d,placeholder:"Back shortly — upgrading the server",onChange:H=>{c(H.target.value),p(!0)}})})}),t?null:e.jsxs(M,{title:"Quiet time",intro:`Pause new television requests and server background work every day in ${((le=s==null?void 0:s.quietTime)==null?void 0:le.timeZone)??"the server timezone"}. Work already under way finishes safely. The admin console and health checks stay available so the schedule can always be changed.`,icon:"clock",tone:(me=s==null?void 0:s.quietTime)!=null&&me.active?"warn":"info",actions:(ie=s==null?void 0:s.quietTime)!=null&&ie.active?e.jsx(N,{tone:"warn",children:"active now"}):g?e.jsx(N,{tone:"ok",children:"scheduled"}):e.jsx(N,{children:"off"}),footer:e.jsx(T,{variant:"primary",busy:l==="quiet",onClick:()=>void L(),children:"Save quiet time"}),children:[e.jsx(Z,{label:"Pause server activity during quiet time",checked:g,onChange:H=>{b(H),R(!0)}}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Starts",hint:"Uses the server's 24-hour clock.",children:e.jsx("input",{type:"time",value:h,onChange:H=>{m(H.target.value),R(!0)}})}),e.jsx(D,{label:"Ends",hint:"May be on the following day, for example 23:00 to 07:00.",children:e.jsx("input",{type:"time",value:y,onChange:H=>{v(H.target.value),R(!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:x,placeholder:"Quiet time — try again after 7 am",onChange:H=>{$(H.target.value),R(!0)}})})]}),o?e.jsx(ke,{title:"Take Memby offline?",body:"Every television will stop working immediately and show your message in place of the launcher. This console keeps working.",confirmLabel:"Go offline",destructive:!0,busy:l==="on",onConfirm:()=>void A(!0),onCancel:()=>u(!1)}):null]})}const Ys=s=>s==="Unattributed"?"warn":s==="Go runtime"?"note":"info";function Ri(){const s=Y("/admin/api/runtime",{pollMs:3e4}),[n,t]=f.useState(),[a,i]=f.useState(!1),[l,r]=f.useState(""),d=f.useCallback(async()=>{i(!0),r("");try{t(await q.get("/admin/api/runtime/goroutines"))}catch(b){r(b instanceof Error?b.message:String(b))}finally{i(!1)}},[]);if(s.loading||!s.data)return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Runtime",intro:"What the gateway process is doing."}),e.jsx(P,{message:s.error}),e.jsx(B,{})]});const c=s.data,o=c.memory,u=c.samples??[],j=c.workers??[],p=j.filter(b=>b.state==="running").length,g=o.memoryLimit>0&&o.memoryLimitw(Math.round(b)))}`,value:w(c.goroutines),icon:"pulse",tone:c.goroutineTrend.direction==="rising"?"warn":"info"},{label:"named workers running",value:w(p),icon:"clock",tone:"note"},{label:`heap in use · ${rs(c.heapTrend,he)}`,value:he(o.heapInuse),icon:"chip",tone:o.heapShare>=.75?"warn":"data"},{label:"running for",value:An(c.uptimeSeconds),icon:"history",tone:"data"}]}),e.jsxs(ue,{cols:"2",children:[e.jsxs(M,{title:"Memory",intro:"Three different figures that are routinely confused. Heap in use is what the gateway is actually holding. Reserved is what the Go runtime has taken from the operating system on its behalf — always larger, and not a leak. The limit is the one the container is stopped at.",icon:"chip",tone:"data",children:[g?e.jsxs(e.Fragment,{children:[e.jsx($n,{value:o.heapInuse,total:o.memoryLimit,tone:o.heapShare>=.9?"bad":o.heapShare>=.75?"warn":"ok"}),e.jsxs("p",{className:"hint",children:["The heap is using ",(o.heapShare*100).toFixed(1),"% of the"," ",he(o.memoryLimit)," limit",o.configuredLimit?` set by GOMEMLIMIT=${o.configuredLimit}`:"","."]})]}):e.jsx(xe,{tone:"warn",children:"No memory limit is set, so the Go runtime will grow until the container is killed by the host. Set GOMEMLIMIT to give it a ceiling to collect against."}),e.jsx(We,{rows:[{label:"Heap currently used",value:he(o.heapInuse)},{label:"Heap allocated to live objects",value:he(o.heapAlloc)},{label:"Heap held but idle",value:he(o.heapIdle)},{label:"Returned to the operating system",value:he(o.heapReleased)},{label:"Goroutine stacks",value:he(o.stackInuse)},{label:"Runtime / system reserved",value:he(o.sys)},{label:"Configured process limit",value:g?he(o.memoryLimit):"none"},{label:"Next collection at",value:he(o.nextGc)}]})]}),e.jsxs(M,{title:"Collection",intro:"How often the garbage collector runs and how long it stops the gateway for. Everything the televisions ask for waits during a pause, so this is the figure that turns a memory problem into a slow one.",icon:"refresh",tone:"note",children:[e.jsx(Cs,{tiles:[{label:"collections",value:w(o.numGc)},{label:"collections an hour",value:o.gcPerHour>0?o.gcPerHour.toFixed(0):"—"},{label:"recent pause",value:`${o.pauseRecentMs.toFixed(1)} ms`},{label:"paused in total",value:`${(o.pauseTotalMs/1e3).toFixed(1)} s`}]}),e.jsx(We,{rows:[{label:"Processors available",value:w(c.gomaxprocs)},{label:"Operating-system threads",value:w(c.threads)},{label:"Processor use",value:c.process.cpuKnown?`${(c.process.cpuPercent??0).toFixed(1)}% of one processor · ${(c.process.cpuSeconds??0).toFixed(0)}s used in total`:"not available on this host"},{label:"Open connections",value:c.process.filesKnown?`${w(c.process.openSockets)} sockets of ${w(c.process.openFiles)} open files${c.process.fileLimit?` · limit ${w(c.process.fileLimit)}`:""}`:"not available on this host"},{label:"Go version",value:c.goVersion}]})]})]}),e.jsxs(ue,{cols:"2",children:[e.jsx(M,{title:"Goroutines over time",intro:"A single instantaneous count cannot show a leak. This can: a line that climbs and never comes back down is work the gateway is not letting go of.",icon:"chart",tone:"info",children:e.jsx(Js,{samples:u,valueOf:b=>b.goroutines,format:b=>w(b),everySeconds:c.sampleEverySeconds})}),e.jsx(M,{title:"Memory over time",intro:"Heap in use, sampled on the same tick. A saw-tooth that returns to roughly the same floor after each collection is healthy; a floor that keeps rising is not.",icon:"chart",tone:"data",children:e.jsx(Js,{samples:u,valueOf:b=>b.heapInuse,format:he,everySeconds:c.sampleEverySeconds})})]}),e.jsx(M,{title:"Background workers",intro:"Memby's own long-running work, named where it is started rather than guessed at from a stack. A worker that has finished is not necessarily a fault — several are one-shot startup jobs — but one that keeps being started again is failing at something.",icon:"clock",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Worker"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Since"}),e.jsx("th",{className:"num",children:"Starts"})]})}),e.jsx("tbody",{children:j.length===0?e.jsx(J,{columns:5,children:"Nothing has registered. This gateway predates named workers."}):j.map(b=>e.jsx(Ei,{worker:b},b.name))})]})})}),e.jsxs(M,{title:"Goroutine breakdown",intro:"What those goroutines are actually doing, and which part of Memby they belong to. Collecting it means walking every stack, which stops the gateway for a few milliseconds — so it is taken when you ask rather than continuously.",icon:"search",tone:"info",children:[e.jsxs("div",{className:"row",children:[e.jsx(T,{onClick:d,busy:a,icon:"refresh",variant:"primary",children:n?"Collect again":"Collect breakdown"}),e.jsx("a",{href:"/admin/api/runtime/goroutines?format=text",download:!0,children:e.jsx(T,{icon:"download",variant:"quiet",children:"Download full stack dump"})})]}),e.jsx(P,{message:l}),n?e.jsx(Ai,{report:n}):e.jsx(ee,{children:"Nothing collected yet. The snapshot is a moment in time, so it is taken on request and stamped with when it was taken."})]})]})}function Ei({worker:s}){const n=s.state==="finished";return e.jsxs("tr",{children:[e.jsx("td",{children:s.name}),e.jsx("td",{children:e.jsx(N,{tone:"info",children:s.component})}),e.jsx("td",{children:e.jsx(N,{tone:n?"note":"ok",children:n?"finished":"running"})}),e.jsx("td",{children:F(n?s.stopped:s.started)}),e.jsx("td",{className:"num",children:s.starts>1?e.jsx(N,{tone:"warn",children:s.starts}):s.starts})]})}function Js({samples:s,valueOf:n,format:t,everySeconds:a}){if(s.length<2)return e.jsxs(ee,{children:["Not enough history yet. Readings are taken every"," ",a>=60?`${Math.round(a/60)} minutes`:`${a}s`,", and a trend needs about fifteen minutes of them."]});const i=l=>new Date(l.at).toLocaleTimeString();return e.jsx(hs,{data:s,labelOf:l=>i(l),valueOf:l=>n(l),title:l=>`${i(l)}: ${t(n(l))}`})}function Ai({report:s}){const n=s.categories??[],t=s.components??[],a=s.groups??[];return e.jsxs(e.Fragment,{children:[e.jsxs("p",{className:"hint",children:[w(s.total)," goroutines, collected ",F(s.at)," in"," ",s.collectedInMs.toFixed(1)," ms."]}),e.jsxs(ue,{cols:"2",children:[e.jsxs("div",{children:[e.jsx(xs,{children:"By what they are doing"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"State"}),e.jsx("th",{className:"num",children:"Count"})]})}),e.jsx("tbody",{children:n.map(i=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:i.label}),e.jsx("p",{className:"hint",children:i.description}),e.jsx("p",{className:"hint",children:(i.states??[]).map(l=>`${l.state} (${l.count})`).join(" · ")})]}),e.jsx("td",{className:"num",children:w(i.count)})]},i.category))})]})})]}),e.jsxs("div",{children:[e.jsx(xs,{children:"By which part of Memby"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Area"}),e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:t.map(i=>e.jsxs("tr",{children:[e.jsx("td",{children:e.jsx(N,{tone:Ys(i.component),children:i.component})}),e.jsx("td",{className:"num",children:w(i.count)}),e.jsx("td",{className:"num",children:i.longestWaitMinutes>0?`${w(i.longestWaitMinutes)} min`:"—"})]},i.component))})]})})]})]}),e.jsx(xs,{aside:s.groupsTotal>a.length?`the ${a.length} largest of ${w(s.groupsTotal)}`:void 0,children:"Where they are waiting"}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"num",children:"Count"}),e.jsx("th",{children:"Area"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Function"}),e.jsx("th",{className:"num",children:"Oldest"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(J,{columns:5,children:"Nothing to group."}):a.map((i,l)=>e.jsxs("tr",{children:[e.jsx("td",{className:"num",children:w(i.count)}),e.jsx("td",{children:e.jsx(N,{tone:Ys(i.component),children:i.component})}),e.jsx("td",{children:i.state}),e.jsxs("td",{children:[e.jsx("span",{className:"mono",children:i.function}),e.jsx("p",{className:"hint",children:i.file})]}),e.jsx("td",{className:"num",children:i.longestWaitMinutes>0?`${w(i.longestWaitMinutes)} min`:"—"})]},`${i.component}-${i.function}-${i.state}-${l}`))})]})})]})}const Ii=-1;function De(s){return s===0?"":s<0?"off":String(s)}function qe(s,n){const t=s.trim().toLowerCase();if(t==="")return 0;if(n&&(t==="off"||t==="none"||t==="0"))return Ii;const a=Number.parseInt(t,10);return Number.isFinite(a)?a:0}function Fe(s,n){return s<=0?"off":`${s} ${n}${s===1?"":"s"}`}function Qs(s){switch(s){case"everywhere":return"Everywhere";case"off":return"Off";default:return"Home only"}}function gs(s){return{timezone:s.timezone??"",logLevel:s.logLevel??"",sessionIdleDays:De(s.sessionIdleDays),sonarrAlertMinutes:De(s.sonarrAlertMinutes),radarrAlertMinutes:De(s.radarrAlertMinutes),notificationDisplay:s.notificationDisplay||"home_only",embyHealthSeconds:De(s.embyHealthSeconds),slowRequestMillis:De(s.slowRequestMillis),librarySyncMinutes:De(s.librarySyncMinutes)}}function Li(){const{data:s,error:n,loading:t,reload:a}=Y("/admin/api/gateway-settings"),{wrap:i}=te(),{busy:l,run:r}=se(),[d,c]=f.useState(null);f.useEffect(()=>{!d&&s&&c(gs(s.settings))},[s,d]);const o=(m,y)=>c(v=>v&&{...v,[m]:y}),u=()=>r("save",async()=>{if(!d)return;const m={timezone:d.timezone.trim(),logLevel:d.logLevel.trim(),sessionIdleDays:qe(d.sessionIdleDays,!1),sonarrAlertMinutes:qe(d.sonarrAlertMinutes,!0),radarrAlertMinutes:qe(d.radarrAlertMinutes,!0),notificationDisplay:d.notificationDisplay,embyHealthSeconds:qe(d.embyHealthSeconds,!0),slowRequestMillis:qe(d.slowRequestMillis,!0),librarySyncMinutes:qe(d.librarySyncMinutes,!0)},y=await i(()=>q.post("/admin/api/gateway-settings",m),"Gateway settings saved.");y&&c(gs(y.settings)),await a()}),j=()=>r("clear",async()=>{const m=await i(()=>q.post("/admin/api/gateway-settings",{timezone:"",logLevel:"",sessionIdleDays:0,sonarrAlertMinutes:0,radarrAlertMinutes:0,embyHealthSeconds:0,slowRequestMillis:0,librarySyncMinutes:0,notificationDisplay:"home_only"}),"Every setting is back to its deployed or server default.");m&&c(gs(m.settings)),await a()}),p=s==null?void 0:s.deployed,g=s==null?void 0:s.effective,b=(s==null?void 0:s.logLevels)??[],h=(s==null?void 0:s.notificationDisplays)??["everywhere","home_only","off"];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Gateway settings",intro:"Server-level settings for this gateway, changeable without a redeployment."}),e.jsx(P,{message:n}),t||!d||!p||!g?e.jsx(B,{rows:2}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"This gateway",intro:"What the process is running and what it currently believes.",icon:"chip",tone:"info",actions:e.jsx(N,{tone:"info",children:(s==null?void 0:s.version)??"unknown"}),children:e.jsx(We,{rows:[{label:"Server timezone",value:g.timezone||"not set"},{label:"Log level",value:g.logLevel},{label:"Sign-in expiry",value:Fe(g.sessionIdleDays,"day")},{label:"Emby health probe",value:Fe(g.embyHealthSeconds,"second")},{label:"Slow-request breakdown",value:Fe(g.slowRequestMillis,"millisecond")},{label:"Catalogue sweep",value:Fe(g.librarySyncMinutes,"minute")},{label:"Episode alert window",value:Fe(g.sonarrAlertMinutes,"minute")},{label:"Film alert window",value:Fe(g.radarrAlertMinutes,"minute")},{label:"Notification display",value:Qs(g.notificationDisplay)}]})}),e.jsxs(M,{title:"Server settings",intro:"Environment-backed fields can be left empty to use the deployed value shown beneath them. Changes take effect immediately — nothing here needs a restart.",icon:"sliders",tone:"note",footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:l==="save",onClick:()=>void u(),children:"Save settings"}),e.jsx(T,{busy:l==="clear",onClick:()=>void j(),children:"Use defaults"})]}),children:[e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Server timezone",hint:`Deployed: ${p.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:p.timezone,onChange:m=>o("timezone",m.target.value)})}),e.jsx(D,{label:"Log level",hint:`Deployed: ${p.logLevel}. Applies to the running process at once, so debug can be turned on to watch something happen.`,children:e.jsxs("select",{value:d.logLevel,onChange:m=>o("logLevel",m.target.value),children:[e.jsxs("option",{value:"",children:["Deployed (",p.logLevel,")"]}),b.map(m=>e.jsx("option",{value:m,children:m},m))]})})]}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Sign a television out after (days)",hint:`Deployed: ${p.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(p.sessionIdleDays),onChange:m=>o("sessionIdleDays",m.target.value)})}),e.jsx(D,{label:"Slow-request breakdown above (ms)",hint:`Deployed: ${p.slowRequestMillis||"off"}. A request slower than this logs where its time went — Emby, Postgres, Redis, row assembly. Lower it while chasing one slow screen; type off to stop annotating altogether.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.slowRequestMillis,placeholder:String(p.slowRequestMillis),onChange:m=>o("slowRequestMillis",m.target.value)})}),e.jsx(D,{label:"Emby health probe (seconds)",hint:`Deployed: ${p.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(p.embyHealthSeconds),onChange:m=>o("embyHealthSeconds",m.target.value)})})]}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Catalogue sweep (minutes)",hint:`Deployed: ${p.librarySyncMinutes||"off"}. How often the gateway asks Emby what has changed. With the Sonarr and Radarr webhooks wired up a new file is in the catalogue within a minute of landing, and this is only reconciliation for media they do not manage — 360 is a sensible choice then. Without them it is the only way anything is found.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.librarySyncMinutes,placeholder:String(p.librarySyncMinutes),onChange:m=>o("librarySyncMinutes",m.target.value)})})}),e.jsx("div",{className:"fields",children:e.jsx(D,{label:"Notification display",hint:"Controls informational banners for every viewer and television. Everywhere also permits playback overlays; Home only keeps them off active movies, episodes and trailers; Off hides them throughout Memby.",children:e.jsx("select",{value:d.notificationDisplay,onChange:m=>o("notificationDisplay",m.target.value),children:h.map(m=>e.jsx("option",{value:m,children:Qs(m)},m))})})}),e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Episode alert window (minutes)",hint:`Deployed: ${p.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(p.sonarrAlertMinutes),onChange:m=>o("sonarrAlertMinutes",m.target.value)})}),e.jsx(D,{label:"Film alert window (minutes)",hint:`Deployed: ${p.radarrAlertMinutes||"off"}. The same, for a film Radarr has just imported.`,children:e.jsx("input",{type:"text",inputMode:"numeric",value:d.radarrAlertMinutes,placeholder:String(p.radarrAlertMinutes),onChange:m=>o("radarrAlertMinutes",m.target.value)})})]}),e.jsxs(xe,{tone:"note",children:["These settings live in the database and survive a restart. A deployment rewrites",e.jsx("code",{children:".env"}),", not this document, so environment-backed values can then disagree; permanent environment changes belong in ",e.jsx("code",{children:".env.example"})," as well."]}),s!=null&&s.settings.updatedBy?e.jsxs(xe,{children:["Last changed by ",s.settings.updatedBy,s.settings.updatedAt?` on ${new Date(s.settings.updatedAt).toLocaleString("en-NZ")}`:"","."]}):null]})]})]})}const Di={done:"ok",pending:"warn",failed:"bad"};function qi(s){const n=s.payload??{};if(n.series){const t=n.episode&&n.episode>0?` S${String(n.season??0).padStart(2,"0")}E${String(n.episode).padStart(2,"0")}`:"";return`${n.series}${t}`}return n.title?n.year?`${n.title} (${n.year})`:n.title:s.key}function Fi(){const{data:s,error:n,loading:t}=Y("/admin/api/ingest",{pollMs:15e3});if(t)return e.jsx(B,{rows:1});const a=!!(s!=null&&s.sonarrConfigured||s!=null&&s.radarrConfigured),i=(s==null?void 0:s.recent)??[];return e.jsxs(e.Fragment,{children:[e.jsx(P,{message:n}),e.jsxs(M,{title:"Webhook activity",intro:"Sonarr and Radarr are what put files on disk, so they are what the catalogue learns from. A notification is recorded the moment it arrives and read into the catalogue once the file has settled — which is why an import appears here before it appears in Emby.",icon:"plug",tone:"info",children:[e.jsx(ae,{tiles:[{label:"Sonarr webhook",value:s!=null&&s.sonarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.sonarrConfigured?"ok":"warn",icon:"tv"},{label:"Radarr webhook",value:s!=null&&s.radarrConfigured?"Configured":"Not configured",small:!0,tone:s!=null&&s.radarrConfigured?"ok":"warn",icon:"play"},{label:"Waiting",value:w((s==null?void 0:s.counts.pending)??0),icon:"clock",tone:"note"},{label:"Given up on",value:w((s==null?void 0:s.counts.failed)??0),icon:"alert",tone:"bad"},{label:"Settle delay",value:`${(s==null?void 0:s.settleSeconds)??0}s`,small:!0,icon:"history",tone:"data"}]}),a?null:e.jsxs("p",{className:"muted",children:["Neither hook has a token, so both answer 404 and nothing is recorded here. Set MEMBY_SONARR_WEBHOOK_TOKEN and MEMBY_RADARR_WEBHOOK_TOKEN, then point each *arr at"," ",e.jsx("code",{children:"/hooks/sonarr"})," and ",e.jsx("code",{children:"/hooks/radarr"}),". Until then the catalogue sweep below is the only way a new title is found."]}),e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"What"}),e.jsx("th",{children:"Why"}),e.jsx("th",{children:"State"}),e.jsx("th",{children:"Outcome"}),e.jsx("th",{className:"num",children:"Tries"}),e.jsx("th",{children:"Notes"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(J,{columns:8,children:a?"Nothing has been imported, upgraded, renamed or deleted since this was switched on.":"No webhook is configured."}):i.map(l=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(l.updatedAt)}),e.jsx("td",{className:"muted",children:l.source||"—"}),e.jsx("td",{children:qi(l)}),e.jsx("td",{className:"muted",children:l.reason}),e.jsx("td",{children:e.jsx(N,{tone:Di[l.state]??"warn",children:l.state})}),e.jsx("td",{className:"muted",children:l.outcome||"—"}),e.jsx("td",{className:"num",children:l.attempts}),e.jsx("td",{className:"muted",children:l.lastError||""})]},l.key))})]})})]})]})}function Oi(){const{status:s,error:n,loading:t}=je(),a=(s==null?void 0:s.runs)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Imports",intro:"What Sonarr and Radarr said changed, and the catalogue sweep that reconciles everything they do not manage."}),e.jsx(P,{message:n}),e.jsx(Fi,{}),t?e.jsx(B,{rows:1}):e.jsx(M,{title:"Synchronisation history",intro:"A full import mark-and-sweeps the catalogue; an incremental one asks Emby for what changed, with a minute of overlap so nothing falls between two runs. With both webhooks wired up this is reconciliation — media dropped in by hand, a title edited in Emby, a notification that never arrived — rather than how new titles are found.",icon:"sync",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Started"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{children:"Trigger"}),e.jsx("th",{children:"Status"}),e.jsx("th",{className:"num",children:"Seen"}),e.jsx("th",{className:"num",children:"Written"}),e.jsx("th",{className:"num",children:"Removed"}),e.jsx("th",{children:"Notes"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(J,{columns:8,children:"Nothing has been imported yet."}):a.map(i=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",children:F(i.startedAt)}),e.jsx("td",{children:i.kind}),e.jsx("td",{className:"muted",children:i.trigger}),e.jsx("td",{children:e.jsx(N,{tone:i.status==="success"?"ok":i.status==="running"?"warn":"bad",children:i.status})}),e.jsx("td",{className:"num",children:w(i.itemsSeen)}),e.jsx("td",{className:"num",children:w(i.itemsUpserted)}),e.jsx("td",{className:"num",children:w(i.itemsRemoved)}),e.jsx("td",{className:"muted",children:i.error||""})]},i.id||i.startedAt))})]})})})]})}const Pi={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"},Ln=s=>Pi[s]??"quiet",_i={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"]},Ui=[[/^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 Wi(s,n){const t=n.toLowerCase();for(const[i,l]of Ui)if(i.test(t))return l;const a=_i[s];return a||(s?["gateway","Gateway",ms(s.replace(/[-_]/g," "))]:["gateway","Gateway","Server"])}const Bi={h:36e5,m:6e4,s:1e3,ms:1,us:.001,µs:.001,ns:1e-6};function bs(s){if(typeof s=="number")return Number.isFinite(s)?s:null;if(typeof s!="string"||!s)return null;const n=s.matchAll(/([0-9]*\.?[0-9]+)(ns|µs|us|ms|h|m|s)/g);let t=0,a=!1;for(const i of n){const l=i[2]?Bi[i[2]]:void 0;l!==void 0&&(t+=Number(i[1])*l,a=!0)}return a?t:null}function Ts(s){return s<1?"<1 ms":s<1e3?`${Math.round(s)} ms`:s<1e4?`${(s/1e3).toFixed(1)} s`:s<6e4?`${Math.round(s/1e3)} s`:`${Math.floor(s/6e4)}m ${Math.round(s%6e4/1e3)}s`}const Vi=s=>s>=3e3?"bad":s>=1e3?"warn":null,Hi={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 zi(s){const n=Hi[s];return n||(s>=500?"Server Error":s>=400?"Client Error":s>=300?"Redirected":s>=200?"OK":"Response")}const Ki=s=>s>=500?"bad":s>=400?"warn":s>=300?"quiet":"ok",Gi={"/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"},Zi=s=>/\d/.test(s)||s.length>24;function Xs(s){const n=Gi[s];if(n)return n;const t=s.split("/").filter(i=>i&&i!=="v1"&&i!=="api");t[0]==="admin"&&t.shift();const a=t.filter(i=>!Zi(i));return a.length===0?s:ms(a.join(" ").replace(/[-_.]/g," ").replace(/\s+/g," ").trim())}const ms=s=>s&&s.charAt(0).toUpperCase()+s.slice(1),es=s=>ms(s.replace(/_/g," ")),ve=s=>s==null?"":String(s),Dn=new Set(["","unknown","none","null","","0"]),Me=s=>!Dn.has(ve(s).toLowerCase()),Yi=["title","series","name","query","item_title","file"],Ji={directplay:"ok",direct:"ok",directstream:"ok",transcode:"warn",transcoding:"warn"};function Qi(s,n,t){if(t!==null)return{label:`${t} ${zi(t)}`,short:String(t),tone:Ki(t)};if(Me(s.error))return{label:"Failed",short:"Failed",tone:n==="WARN"?"warn":"bad"};const a=ve(s.play_method).toLowerCase().replace(/[\s_-]/g,"");if(a&&!Dn.has(a)){const i=ms(ve(s.play_method).replace(/([a-z])([A-Z])/g,"$1 $2"));return{label:i,short:i,tone:Ji[a]??"info"}}if(Me(s.cache)){const i=/hit|true|yes/i.test(ve(s.cache));return{label:i?"Cached":"Cache miss",short:i?"Cached":"Miss",tone:i?"data":"quiet"}}return n==="ERROR"?{label:"Failed",short:"Failed",tone:"bad"}:n==="WARN"?{label:"Warning",short:"Warning",tone:"warn"}:null}function Xi(s){const n=[];Me(s.user)&&n.push(ve(s.user)),Me(s.device)&&n.push(ve(s.device));const t=bs(s.marker_ms);t!==null&&t>0&&n.push(`Start ${en(t)}`);const a=bs(s.position);return a!==null&&a>0&&n.push(`At ${en(a)}`),Me(s.watched)&&n.push(`${ve(s.watched)} watched`),Me(s.reason)&&n.push(ve(s.reason)),n.slice(0,3).join(" · ")}function en(s){const n=Math.round(s/1e3),t=Math.floor(n/3600),a=Math.floor(n%3600/60),i=n%60,l=r=>String(r).padStart(2,"0");return t>0?`${t}:${l(a)}:${l(i)}`:`${a}:${l(i)}`}const qn=[{title:"Request",keys:["method","path","query_keys","status","cache","client","protocol","host"]},{title:"Context",keys:["user","user_id","device","device_id","item","title","series","type","play_method","play_session_id","media_source_id","position","resume","runtime","watched","subtitles","subtitle_track","subtitle_language","event_name"]},{title:"Where the time went",keys:["duration","breakdown","elsewhere"]},{title:"Diagnostics",keys:["error","stack","correlation","version","gateway_version"]}],ea=new Set(qn.flatMap(s=>s.keys)),sa=s=>ea.has(s),na=new Intl.DateTimeFormat("en-NZ",{hour:"2-digit",minute:"2-digit",second:"2-digit",hour12:!1}),ta=new Intl.DateTimeFormat("en-NZ",{weekday:"short",day:"numeric",month:"short"}),sn=new WeakMap;function Ze(s){const n=sn.get(s);if(n)return n;const t=ia(s);return sn.set(s,t),t}function ia(s){const n=s.attributes??{},t=s.message??"",[a,i,l]=Wi(ve(n.component),t),r=ve(n.path),d=r?ve(n.method).toUpperCase():"",c=Number(n.status),o=r&&Number.isFinite(c)&&c>0?c:null,u=t==="request"&&!!r,j=Yi.find(L=>Me(n[L])),p=j?ve(n[j]):"";let g,b;u?(g=d||"HTTP",b=Xs(r)):r&&d?(g=d,b=p?`${es(t)} · ${p}`:`${es(t)} — ${Xs(r)}`):(g="",b=p?`${es(t)} · ${p}`:es(t));const h=bs(n.duration??n.duration_ms??n.negotiation_duration),m=Me(n.error)?ve(n.error):"",y=Xi(n),v=new Date(s.occurredAt),x=ve(n.breakdown),$=m||(u?x:y),k=m?"error":$?"context":"",R=!m&&u?y:"",I=Object.entries(n);return{serviceKey:a,service:i,component:l,action:g,summary:b,context:y,detail:m,secondary:$,secondaryTone:k,trail:R,result:Qi(n,s.level,o),durationMs:h,method:d,status:o,eventKey:t,level:s.level,time:`${na.format(v)}.${String(v.getMilliseconds()).padStart(3,"0")}`,day:ta.format(v),dayKey:v.toDateString(),tall:!!$,haystack:[t,i,l,b,y,m,...I.flat().map(ve)].join(" ").toLowerCase(),fields:I,attributes:n}}const nn={TRACE:5,DEBUG:10,INFO:20,WARN:30,ERROR:40},ss={level:"INFO",service:"",component:"",event:"",method:"",status:"",slower:0,text:""};function aa(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 ra(s,n,t){if((nn[s.level]??0)<(nn[n.level]??20))return!1;const a=Ze(s);return!(n.service&&a.serviceKey!==n.service||n.component&&a.component!==n.component||n.event&&a.eventKey!==n.event||n.method&&a.method!==n.method||!aa(n.status,a.status)||n.slower>0&&(a.durationMs??0)({key:l,label:r})).sort((l,r)=>l.label.localeCompare(r.label)),components:[...t].sort((l,r)=>l.localeCompare(r)),events:[...a].sort((l,r)=>l.localeCompare(r)),methods:[...i].sort((l,r)=>l.localeCompare(r))}}function oa(s,n){var a;const t=[];if(s.service){const i=((a=n.find(l=>l.key===s.service))==null?void 0:a.label)??s.service;t.push({key:"service",label:`Service: ${i}`})}return s.component&&t.push({key:"component",label:`Component: ${s.component}`}),s.event&&t.push({key:"event",label:`Event: ${s.event}`}),s.method&&t.push({key:"method",label:`Method: ${s.method}`}),s.status&&t.push({key:"status",label:`Status: ${s.status==="error"?"≥400":s.status}`}),s.slower>0&&t.push({key:"slower",label:`Duration: >${Ts(s.slower)}`}),s.text&&t.push({key:"text",label:`Search: ${s.text}`}),t}const ns=2e4,ca=5e3,tn=7,da=16,ha=15,ua=2,ma=1,ys=tn+da+tn+ma,an=ys+ua+ha,rn=26,pa=31,ln=10,xa=[{value:"TRACE",label:"Everything"},{value:"DEBUG",label:"Debug+"},{value:"INFO",label:"Info+"},{value:"WARN",label:"Warnings+"},{value:"ERROR",label:"Errors only"}],ja=[{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)"}],ga=[{value:0,label:"Any duration"},{value:100,label:"Slower than 100 ms"},{value:500,label:"Slower than 500 ms"},{value:1e3,label:"Slower than 1 s"},{value:3e3,label:"Slower than 3 s"}],on=s=>s.replace(/_/g," ");function ts({onPick:s,className:n,title:t,children:a,...i}){return e.jsx("button",{type:"button",className:`logfacet ${n}`,title:t,onClick:s,...i,children:a})}const va=f.memo(function({event:n,view:t,top:a,height:i,selected:l,onInspect:r,onFilter:d}){const c=t.durationMs!==null?Vi(t.durationMs):null;return e.jsxs("div",{className:"logrow","data-level":t.level,"data-selected":l||void 0,style:{transform:`translateY(${a}px)`,height:`${i}px`},children:[e.jsx("time",{className:"logrow-time",title:n.occurredAt,children:t.time}),e.jsx(ts,{className:"logrow-level","data-level":t.level,title:`Show ${t.level} and above`,onPick:()=>d({level:t.level}),children:t.level}),e.jsxs("span",{className:"logrow-place",children:[e.jsx(ts,{className:"logrow-service","data-tone":Ln(t.serviceKey),title:`Filter to ${t.service}`,onPick:()=>d({service:t.serviceKey,component:""}),children:t.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),e.jsx(ts,{className:"logrow-component",title:`Filter to ${t.component}`,onPick:()=>d({component:t.component}),children:t.component})]}),e.jsxs("button",{type:"button",className:"logrow-summary",title:[t.summary,t.trail,t.secondary].filter(Boolean).join(" — "),onClick:()=>r(n.sequence),children:[e.jsxs("span",{className:"logrow-line",children:[t.action?e.jsx("b",{className:"logrow-action","data-method":t.method||void 0,children:t.action}):null,e.jsx("span",{className:"logrow-text",children:t.summary}),t.trail?e.jsx("span",{className:"logrow-trail",children:t.trail}):null]}),t.secondary?e.jsx("span",{className:"logrow-second","data-tone":t.secondaryTone,children:t.secondaryTone==="error"?`↳ ${t.secondary}`:t.secondary}):null]}),e.jsx("span",{className:"logrow-result",children:t.result?e.jsx(ts,{className:"logrow-verdict","data-tone":t.result.tone,title:t.status!==null?`Filter to ${t.status}`:`Filter to ${t.eventKey}`,onPick:()=>t.status!==null?d({status:`${Math.floor(t.status/100)}xx`}):d({event:t.eventKey}),children:t.result.label}):null}),e.jsx("span",{className:"logrow-duration","data-tone":c??void 0,children:t.durationMs!==null?Ts(t.durationMs):""})]})});function fa({event:s,view:n,onClose:t}){const[a,i]=f.useState(!1),l=n.fields.filter(([c])=>!sa(c)&&c!=="component"),r=async()=>{try{await navigator.clipboard.writeText(JSON.stringify(s,null,2)),i(!0),window.setTimeout(()=>i(!1),1600)}catch{i(!1)}},d=qn.map(c=>({title:c.title,rows:c.keys.map(o=>[o,n.attributes[o]]).filter(([,o])=>o!=null&&String(o)!=="")})).filter(c=>c.rows.length>0);return e.jsxs("section",{className:"logdrawer","aria-label":`Log record ${s.sequence}`,children:[e.jsxs("header",{className:"logdrawer-head",children:[e.jsxs("div",{children:[e.jsxs("p",{className:"logdrawer-place",children:[e.jsx("span",{className:"logrow-service","data-tone":Ln(n.serviceKey),children:n.service}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),n.component]}),e.jsx("b",{children:n.summary}),n.detail?e.jsx("p",{className:"logdrawer-error",children:n.detail}):null]}),e.jsxs("div",{className:"logdrawer-actions",children:[e.jsx(T,{size:"sm",variant:"quiet",onClick:r,icon:"download",children:a?"Copied":"Copy JSON"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:t,icon:"close",children:"Close"})]})]}),e.jsxs("div",{className:"logdrawer-grid",children:[e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Overview"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Time"}),e.jsxs("dd",{children:[n.day," ",n.time]}),e.jsx("dt",{children:"Level"}),e.jsx("dd",{children:n.level}),e.jsx("dt",{children:"Service"}),e.jsxs("dd",{children:[n.service," › ",n.component]}),e.jsx("dt",{children:"Event"}),e.jsx("dd",{children:n.eventKey}),n.result?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Result"}),e.jsx("dd",{children:n.result.label})]}):null,n.durationMs!==null?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Duration"}),e.jsx("dd",{children:Ts(n.durationMs)})]}):null,e.jsx("dt",{children:"Record"}),e.jsxs("dd",{children:["#",s.sequence]})]})]}),d.map(c=>e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:c.title}),e.jsx("dl",{children:c.rows.map(([o,u])=>e.jsxs(f.Fragment,{children:[e.jsx("dt",{children:on(o)}),e.jsx("dd",{children:String(u)})]},o))})]},c.title)),l.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Details"}),e.jsx("dl",{children:l.map(([c,o])=>e.jsxs(f.Fragment,{children:[e.jsx("dt",{children:on(c)}),e.jsx("dd",{children:String(o)})]},c))})]}):null]}),e.jsxs("details",{className:"logdrawer-raw",children:[e.jsx("summary",{children:"Raw event"}),e.jsx("pre",{children:JSON.stringify(s,null,2)})]})]})}function ba(){var He;const[s,n]=f.useState([]),[t,a]=f.useState(0),[i,l]=f.useState(!1),[r,d]=f.useState(0),[c,o]=f.useState(()=>{const C=new URLSearchParams(window.location.search).get("q")??"";return C?{...ss,text:C}:ss}),[u,j]=f.useState(""),[p,g]=f.useState({top:0,height:600}),[b,h]=f.useState(!0),[m,y]=f.useState(null),v=f.useDeferredValue(c.text.trim().toLowerCase()),x=f.useRef(0),$=f.useRef(!1),k=f.useRef(0),R=f.useRef(null),I=f.useRef(!0),A=f.useRef(void 0),L=f.useRef([]),X=f.useRef(i);f.useEffect(()=>{X.current=i},[i]);const le=f.useCallback(C=>{n(V=>{const Q=V.concat(C);return Q.length>ns?Q.slice(Q.length-ns):Q})},[]),me=f.useCallback(async()=>{if($.current||document.hidden)return;$.current=!0;const C=k.current,V=[];let Q=0;try{let be=0,Ce;do Ce=await q.get(`/admin/api/events?after=${x.current}&limit=1000`),x.current=Ce.next||x.current,Q+=Ce.dropped||0,V.push(...Ce.events??[]),be+=1;while(Ce.hasMore&&be<20);j("")}catch(be){j(be instanceof Error?be.message:String(be))}finally{V.length>0&&C===k.current&&(X.current?(L.current=L.current.concat(V),L.current.length>ns&&(L.current=L.current.slice(L.current.length-ns)),d(L.current.length)):le(V)),Q>0&&C===k.current&&a(be=>be+Q),$.current=!1}},[le]);f.useEffect(()=>{let C;const V=()=>{window.clearInterval(C),C=document.hidden?void 0:window.setInterval(()=>void me(),ca)},Q=()=>{V(),document.hidden||me()};return me(),V(),document.addEventListener("visibilitychange",Q),()=>{window.clearInterval(C),document.removeEventListener("visibilitychange",Q)}},[me]);const ie=f.useCallback(()=>{X.current=!0,l(!0)},[]),H=f.useCallback(()=>{X.current=!1;const C=L.current;L.current=[],d(0),l(!1),C.length&&le(C)},[le]),re=f.useCallback(C=>{o(V=>({...V,...C}))},[]),S=f.useMemo(()=>s.filter(C=>ra(C,c,v)),[s,c,v]),O=f.useMemo(()=>{const C=new Float64Array(S.length+1),V=new Uint8Array(S.length),Q=new Uint8Array(S.length);let be=0,Ce="";for(let Le=0;Le{let C=0,V=S.length;for(;C>1;(O.tops[Q]??0)+(O.heights[Q]??0)<=z?C=Q+1:V=Q}return Math.max(0,C-ln)},[O,z,S.length]),we=f.useMemo(()=>{const C=z+p.height;let V=ce;for(;V{const C=[];for(let V=ce;Vs.find(C=>C.sequence===m),[s,m]),E=f.useCallback(()=>{const C=R.current;C&&(I.current=!0,C.scrollTop=C.scrollHeight,h(!0),g({top:C.scrollTop,height:C.clientHeight}))},[]);f.useLayoutEffect(()=>{const C=R.current;!C||!I.current||(C.scrollTop=C.scrollHeight,g({top:C.scrollTop,height:C.clientHeight}))},[Re,O.total]),f.useEffect(()=>()=>window.cancelAnimationFrame(A.current??0),[]);const _=()=>{const C=R.current;if(!C)return;const V=C.scrollHeight-C.scrollTop-C.clientHeight{g({top:C.scrollTop,height:C.clientHeight})})},de=()=>{const C=new Blob([JSON.stringify(S,null,2)],{type:"application/json"}),V=document.createElement("a");V.href=URL.createObjectURL(C),V.download=`memby-events-${new Date().toISOString().replace(/[:.]/g,"-")}.json`,V.click(),window.setTimeout(()=>URL.revokeObjectURL(V.href),1e3)},pe=f.useMemo(()=>la(s),[s]),Ie=oa(c,pe.services);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Server logs",intro:"Structured gateway events as they happen."}),e.jsx(P,{message:u}),e.jsxs(M,{children:[e.jsxs("div",{className:"logbar",children:[e.jsxs("div",{className:"logbar-filters",children:[e.jsxs("select",{"aria-label":"Service",value:c.service,onChange:C=>re({service:C.target.value,component:""}),children:[e.jsx("option",{value:"",children:"All services"}),pe.services.map(C=>e.jsx("option",{value:C.key,children:C.label},C.key))]}),e.jsxs("select",{"aria-label":"Component",value:c.component,onChange:C=>re({component:C.target.value}),children:[e.jsx("option",{value:"",children:"All components"}),pe.components.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Level",value:c.level,onChange:C=>re({level:C.target.value}),children:xa.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Event",value:c.event,onChange:C=>re({event:C.target.value}),children:[e.jsx("option",{value:"",children:"All events"}),pe.events.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Result",value:c.status,onChange:C=>re({status:C.target.value}),children:ja.map(C=>e.jsx("option",{value:C.value,children:C.label},C.value))}),e.jsxs("select",{"aria-label":"Method",value:c.method,onChange:C=>re({method:C.target.value}),children:[e.jsx("option",{value:"",children:"Any method"}),pe.methods.map(C=>e.jsx("option",{value:C,children:C},C))]}),e.jsx("select",{"aria-label":"Duration",value:String(c.slower),onChange:C=>re({slower:Number(C.target.value)}),children:ga.map(C=>e.jsx("option",{value:String(C.value),children:C.label},C.value))}),e.jsxs("label",{className:"logsearch",children:[e.jsx(ne,{name:"search"}),e.jsx("input",{type:"search",value:c.text,"aria-label":"Search logs",placeholder:"Search person, title, service, component, path, request ID…",onChange:C=>re({text:C.target.value})})]})]}),e.jsxs("div",{className:"logbar-actions",children:[e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>i?H():ie(),icon:i?"play":"clock",children:i?r?`Resume (${w(r)})`:"Resume":"Pause"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:()=>{k.current+=1,L.current=[],d(0),n([]),a(0),y(null)},children:"Clear view"}),e.jsx(T,{size:"sm",variant:"quiet",onClick:de,icon:"download",title:"Export the rows matching the current filters, as delivered by the gateway",children:"Export JSON"})]})]}),Ie.length?e.jsxs("div",{className:"logchips",children:[Ie.map(C=>e.jsxs("button",{type:"button",className:"logchip",onClick:()=>re({[C.key]:ss[C.key]}),children:[C.label,e.jsx(ne,{name:"close"})]},C.key)),e.jsx("button",{type:"button",className:"logchip logchip-clear",onClick:()=>o(ss),children:"Clear all"})]}):null,e.jsxs("div",{className:"logshell",children:[e.jsxs("div",{className:"logview",ref:R,onScroll:_,role:"log","aria-label":"Server events",children:[e.jsxs("div",{className:"loghead","aria-hidden":"true",children:[e.jsx("span",{children:"Time"}),e.jsx("span",{children:"Level"}),e.jsx("span",{children:"Service"}),e.jsx("span",{children:"Event"}),e.jsx("span",{children:"Result"}),e.jsx("span",{children:"Duration"})]}),S.length===0?e.jsx("p",{className:"empty",children:s.length===0?"Waiting for server events…":"No events match these filters."}):e.jsx("div",{className:"logbody",style:{height:`${O.total}px`},children:$e.map(({event:C,view:V,index:Q})=>e.jsxs(f.Fragment,{children:[O.divider[Q]?e.jsx("div",{className:"logday",style:{transform:`translateY(${(O.tops[Q]??0)-rn}px)`},children:e.jsx("span",{children:V.day})}):null,e.jsx(va,{event:C,view:V,top:O.tops[Q]??0,height:O.heights[Q]??ys,selected:C.sequence===m,onInspect:y,onFilter:re})]},C.sequence))})]}),!b&&S.length>0?e.jsxs("button",{type:"button",className:"logtail",onClick:E,children:[e.jsx(ne,{name:"caret"}),"Jump to latest"]}):null]}),e.jsxs("p",{className:"hint",children:[w(s.length)," retained · ",w(S.length)," matching",S.length?` · ${w($e.length)} rows mounted`:"",t?` · ${w(t)} overwritten before delivery`:"",i?` · paused${r?`, ${w(r)} held`:""}`:""]}),ge?e.jsx(fa,{event:ge,view:Ze(ge),onClose:()=>y(null)}):s.length?e.jsx(xe,{children:"Select a row to see the full record — request, context, diagnostics and raw event."}):null]})]})}const cn=["home","movies","shows","favorites","search","recent_searches","genre_browse","for_you","for_you_time","recommendation","continue","latest","my_shows","details","playback","magic_movie","notifications","profiles","settings"];function Ee(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function ya(){const[s,n]=f.useState(30),[t,a]=f.useState(""),i=ws(),l=f.useMemo(()=>`/admin/api/journeys${Ve({days:s,userId:t})}`,[s,t]),{data:r,error:d,loading:c}=Y(l),o=r==null?void 0:r.stats,u=(r==null?void 0:r.users)??[],j=(r==null?void 0:r.actions)??[],p=(r==null?void 0:r.paths)??[],g=p[0],b=f.useMemo(()=>{const h=new Map(((r==null?void 0:r.features)??[]).map(y=>[y.feature,y])),m=new Map(cn.map((y,v)=>[y,v]));return[...new Set([...cn,...h.keys()])].map(y=>({name:y,stat:h.get(y)})).sort((y,v)=>{var $,k;const x=((($=v.stat)==null?void 0:$.uses)??0)-(((k=y.stat)==null?void 0:k.uses)??0);return x||(m.get(y.name)??Number.MAX_SAFE_INTEGER)-(m.get(v.name)??Number.MAX_SAFE_INTEGER)})},[r==null?void 0:r.features]);return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"User journeys",intro:"How viewers move through Memby, use features and complete flows."}),e.jsx(P,{message:d}),e.jsx(M,{title:"Journey health",intro:"Server-derived foreground visits, completion and interruption. Search text, content titles and setting values are never stored.",icon:"people",tone:"info",actions:e.jsxs(e.Fragment,{children:[e.jsx(D,{label:"Window",children:e.jsxs("select",{value:s,onChange: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:t,onChange:h=>{const m=h.target.value;a(m),m&&i(`/admin/journeys/${encodeURIComponent(m)}`)},children:[e.jsx("option",{value:"",children:"All users"}),u.map(h=>e.jsx("option",{value:h.userId,children:h.username||h.userId},h.userId))]})})]}),children:c?e.jsx(B,{rows:1}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:"journeys",value:w(o==null?void 0:o.journeys),icon:"list",tone:"data"},{label:"viewers",value:w(o==null?void 0:o.viewers),icon:"people",tone:"info"},{label:"completion",value:as(o==null?void 0:o.completionRate),icon:"check",tone:"ok"},{label:"abandoned",value:w(o==null?void 0:o.abandoned),icon:"alert",tone:"note"},{label:"active now",value:w(o==null?void 0:o.active),icon:"pulse",tone:"info"},{label:"average steps",value:((o==null?void 0:o.averageSteps)??0).toFixed(1),icon:"chart"},{label:"average visit",value:ye(o==null?void 0:o.averageTimeMs),small:!0,icon:"clock"},{label:"history kept",value:`${(r==null?void 0:r.retentionDays)??90} days`,small:!0,icon:"clock"}]}),e.jsxs("div",{className:"summary-grid",children:[e.jsxs("div",{className:"summary",children:[e.jsxs("div",{className:"summary-head",children:[e.jsx("b",{children:"Visit completion"}),e.jsx("strong",{children:as(o==null?void 0:o.completionRate)})]}),e.jsx($n,{value:(o==null?void 0:o.completed)??0,total:(o==null?void 0:o.journeys)??0}),e.jsxs("p",{children:[w(o==null?void 0:o.completed)," completed · ",w(o==null?void 0:o.abandoned)," abandoned ·"," ",w(o==null?void 0:o.active)," active"]})]}),e.jsxs("div",{className:"summary",children:[e.jsx("div",{className:"summary-head",children:e.jsx("b",{children:"Most common route"})}),e.jsx("strong",{style:void 0,children:g?`${Ee(g.from)} → ${Ee(g.to)}`:"Not enough data"}),e.jsx("p",{children:g?`${w(g.count)} times in this window`:"Journeys will appear here as viewers move through Memby."})]})]})]})}),e.jsxs(ue,{cols:"2",children:[e.jsx(M,{title:"What people do",intro:"Actions show total use and how many separate visits included them.",icon:"chart",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Action"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{className:"num",children:"Visits"})]})}),e.jsx("tbody",{children:j.length===0?e.jsx(J,{columns:3,children:"No significant actions in this window."}):j.map(h=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:Ee(h.action)}),e.jsx("span",{className:"table-sub",children:Ee(h.category)})]}),e.jsx("td",{className:"num",children:w(h.events)}),e.jsx("td",{className:"num",children:w(h.journeys)})]},`${h.category}:${h.action}`))})]})})}),e.jsx(M,{title:"Where people go",intro:"The most common steps between screens, including where quiet visits ended.",icon:"list",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Route"}),e.jsx("th",{className:"num",children:"Times"})]})}),e.jsx("tbody",{children:p.length===0?e.jsx(J,{columns:2,children:"No repeated paths in this window."}):p.map((h,m)=>e.jsxs("tr",{children:[e.jsxs("td",{children:[Ee(h.from)," ",e.jsx("span",{className:"route-arrow",children:"→"})," ",Ee(h.to)]}),e.jsx("td",{className:"num",children:w(h.count)})]},`${h.from}:${h.to}:${m}`))})]})})})]}),e.jsx(M,{title:"Feature use",intro:"Rare and unused features are shown against Memby's major feature catalogue.",icon:"pulse",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Feature"}),e.jsx("th",{className:"num",children:"Uses"}),e.jsx("th",{children:"Last used"}),e.jsx("th",{children:"Status"})]})}),e.jsx("tbody",{children:b.map(({name:h,stat:m})=>{const y=(m==null?void 0:m.uses)??0;return e.jsxs("tr",{children:[e.jsx("td",{children:Ee(h)}),e.jsx("td",{className:"num",children:w(y)}),e.jsx("td",{className:"muted nowrap",children:m?F(m.lastUsedAt):"—"}),e.jsx("td",{children:y===0?e.jsx(N,{tone:"warn",children:"not used"}):y<3?e.jsx(N,{tone:"note",children:"rare"}):e.jsx(N,{tone:"ok",children:"used"})})]},h)})})]})})}),t?null:e.jsx(M,{title:"Inspect a viewer",intro:"Choose a person above to open their dedicated session and viewing-journey timeline.",icon:"journey",tone:"info",children:e.jsx("p",{className:"empty",children:"A viewing journey follows one intent through to playback, so two films watched in a single app session appear as two separate journeys."})})]})}const Te=s=>{const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n},Fn=s=>Te((s==null?void 0:s.target)||(s==null?void 0:s.screen)||(s==null?void 0:s.source)||(s==null?void 0:s.feature)),wa=s=>{const n=s.find(t=>t.category==="playback"&&t.action==="request");return n!=null&&n.source?Te(n.source):Fn(s[0])},dn=s=>s.itemName?`${Te(s.itemType)} · ${s.itemName}`:s.source&&s.target?`${Te(s.source)} → ${Te(s.target)}`:Fn(s),ka=s=>({journey_start:"Opened Memby",home_open:"Opened Memby",journey_end:"Finished session",screen_view:"Viewed",select:"Selected",open:"Opened",close:"Closed",request:s.category==="playback"?"Asked to watch":"Requested",stop:"Left the player",start:s.category==="playback"?s.outcome==="failure"?"Playback failed":"Started watching":"Started",complete:s.category==="playback"?s.outcome==="completed"?"Finished watching":"Stopped watching":"Completed"})[s.action]??Te(s.action);function Na(s){var a;const n=[...s].reverse().find(i=>i.category==="playback"&&i.outcome);if((n==null?void 0:n.outcome)==="failure")return{label:"playback failed",tone:"warn"};if((n==null?void 0:n.outcome)==="completed")return{label:"watched",tone:"ok"};if((n==null?void 0:n.outcome)==="abandoned")return{label:"stopped part-way",tone:"note"};if((n==null?void 0:n.outcome)==="success")return{label:"watched",tone:"ok"};const t=(a=[...s].reverse().find(i=>i.outcome))==null?void 0:a.outcome;return t==="success"||t==="completed"?{label:Te(t),tone:"ok"}:t==="failure"||t==="cancelled"||t==="abandoned"?{label:Te(t),tone:"note"}:s.some(i=>i.action==="stop"&&i.category==="playback")?{label:"watched",tone:"ok"}:{label:"left before playback ended",tone:"warn"}}function Sa(s){const n=s.reduce((t,a,i)=>(a.category==="playback"&&a.action==="request"&&t.push(i),t),[]);return n.length===0?[s]:n.map((t,a)=>s.slice(a===0?0:t,n[a+1]??s.length))}function Ca(){var c,o;const{userId:s=""}=Be(),n=f.useMemo(()=>`/admin/api/journeys${Ve({days:90,userId:s})}`,[s]),{data:t,error:a,loading:i}=Y(n),l=((o=(c=t==null?void 0:t.users)==null?void 0:c.find(u=>u.userId===s))==null?void 0:o.username)||s,r=f.useMemo(()=>{const u=new Map;for(const j of(t==null?void 0:t.events)??[])u.set(j.journeyId,[...u.get(j.journeyId)??[],j]);return[...u.values()].map(j=>j.sort((p,g)=>p.sequence-g.sequence)).sort((j,p)=>{var g,b;return(((g=p[0])==null?void 0:g.occurredAt)??"").localeCompare(((b=j[0])==null?void 0:b.occurredAt)??"")})},[t==null?void 0:t.events]),d=r.flatMap(u=>Sa(u).map((j,p)=>{var g;return{events:j,key:`${(g=u[0])==null?void 0:g.journeyId}:${p}`}}));return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:`${l}'s journeys`,intro:"Each app session is shown as the viewing journeys it contains: entry, selection, playback outcome.",icon:"journey",crumbs:e.jsx(G,{className:"crumb",to:"/admin/journeys",children:"Journeys"})}),e.jsx(P,{message:a}),i?e.jsx(B,{}):e.jsx(M,{title:"Viewing journeys",intro:`${r.length} app session${r.length===1?"":"s"} · ${d.length} viewing journey${d.length===1?"":"s"} in the last 90 days.`,icon:"journey",tone:"info",children:e.jsx("div",{className:"visits",children:d.length===0?e.jsx("p",{className:"empty",children:"No journeys recorded for this viewer."}):d.map((u,j)=>{const p=u.events,g=p[0],b=[...p].reverse().find(m=>m.itemName||m.action==="select"||m.category==="playback"&&m.action==="request"),h=Na(p);return e.jsxs("article",{className:"visit",children:[e.jsxs("header",{children:[e.jsxs("div",{children:[e.jsx("b",{children:F(g==null?void 0:g.occurredAt)}),e.jsxs("span",{children:["Journey ",j+1," · ",p.length," recorded steps"]})]}),e.jsx(N,{tone:h.tone,children:h.label})]}),e.jsxs("div",{className:"journey-answers",children:[e.jsxs("div",{className:"journey-answer","data-kind":"entry",children:[e.jsx(ne,{name:"journey"}),e.jsx("span",{children:"Entered from"}),e.jsx("b",{children:wa(p)})]}),e.jsxs("div",{className:"journey-answer","data-kind":"selection",children:[e.jsx(ne,{name:"play"}),e.jsx("span",{children:"Selected"}),e.jsx("b",{children:b?dn(b):"Nothing selected"})]}),e.jsxs("div",{className:"journey-answer","data-kind":"outcome",children:[e.jsx(ne,{name:h.tone==="ok"?"check":"clock"}),e.jsx("span",{children:"Outcome"}),e.jsx("b",{children:h.label})]})]}),e.jsx("ol",{className:"journey-timeline",children:p.map(m=>e.jsxs("li",{children:[e.jsx("span",{className:"timeline-dot","data-action":m.action}),e.jsxs("div",{children:[e.jsx("b",{children:ka(m)}),e.jsx("span",{children:dn(m)})]}),e.jsx("time",{children:F(m.occurredAt)})]},`${m.journeyId}:${m.sequence}`))})]},u.key)})})})]})}function hn(s){const n=String(s||"—").replaceAll("_"," ");return n==="favorites"?"Favourites":n==="abandoned"?"Abandoned / interrupted":n}function Ma(){const[s,n]=f.useState(30),{data:t,error:a,loading:i}=Y(`/admin/api/analytics?days=${s}`),l=(t==null?void 0:t.rows)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Row engagement",intro:"Impressions, focus, dwell and selections per launcher row."}),e.jsx(P,{message:a}),e.jsx(M,{title:"Launcher rows",intro:"Impressions are rows drawn, focuses are rows the D-pad reached, and dwell is how long it stayed there. Open rate is what a row was worth.",icon:"chart",tone:"info",actions:e.jsx(D,{label:"Window",children:e.jsxs("select",{value:s,onChange:r=>n(Number(r.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"}),e.jsx("option",{value:90,children:"90 days"})]})}),children:i?e.jsx(B,{rows:1}):e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Row"}),e.jsx("th",{children:"Kind"}),e.jsx("th",{className:"num",children:"Dwell"}),e.jsx("th",{className:"num",children:"Impressions"}),e.jsx("th",{className:"num",children:"Focuses"}),e.jsx("th",{className:"num",children:"Opened"}),e.jsx("th",{className:"num",children:"Open rate"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:l.length===0?e.jsx(J,{columns:8,children:"No events in this window."}):l.map(r=>e.jsxs("tr",{children:[e.jsx("td",{children:hn(r.rowId)}),e.jsx("td",{className:"muted",children:hn(r.rowKind)}),e.jsx("td",{className:"num",children:ye(r.dwellMs)}),e.jsx("td",{className:"num",children:w(r.impressions)}),e.jsx("td",{className:"num",children:w(r.focuses)}),e.jsx("td",{className:"num",children:w(r.selects)}),e.jsx("td",{className:"num",children:as(r.selectRate)}),e.jsx("td",{className:"num",children:w(r.viewers)})]},`${r.rowId}:${r.rowKind}`))})]})})})]})}function Ta(){const[s,n]=f.useState(7),{data:t,error:a,loading:i}=Y(`/admin/api/searches?days=${s}`),l=(t==null?void 0:t.terms)??[],r=(t==null?void 0:t.recent)??[],d=t==null?void 0:t.totals;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Searches",intro:"What viewers have been looking for, and what was searched just now."}),e.jsx(P,{message:a}),e.jsx(ae,{tiles:[{label:"searches",value:w((d==null?void 0:d.searches)??0),icon:"search",tone:"info"},{label:"distinct queries",value:w((d==null?void 0:d.queries)??0),icon:"list",tone:"data"},{label:"viewers searching",value:w((d==null?void 0:d.viewers)??0),icon:"people",tone:"note"},{label:"history kept",value:`${(t==null?void 0:t.retentionDays)??30} days`,small:!0,icon:"clock"}]}),i?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(M,{title:"What the house looks for",intro:"Queries the search tab ran, grouped without regard to case and labelled with the most recent spelling. Instant search asks from the second character, so a title typed slowly leaves its prefixes here too.",icon:"search",tone:"info",actions:e.jsx(D,{label:"Window",children:e.jsxs("select",{value:s,onChange:c=>n(Number(c.target.value)),children:[e.jsx("option",{value:1,children:"24 hours"}),e.jsx("option",{value:7,children:"7 days"}),e.jsx("option",{value:30,children:"30 days"})]})}),children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Query"}),e.jsx("th",{className:"num",children:"Searches"}),e.jsx("th",{className:"num",children:"Viewers"}),e.jsx("th",{children:"Last searched"})]})}),e.jsx("tbody",{children:l.length===0?e.jsx(J,{columns:4,children:"Nothing searched in this window."}):l.map(c=>e.jsxs("tr",{children:[e.jsx("td",{children:c.query}),e.jsx("td",{className:"num",children:w(c.searches)}),e.jsx("td",{className:"num",children:w(c.viewers)}),e.jsx("td",{className:"muted nowrap",children:F(c.lastAt)})]},c.query))})]})})}),e.jsx(M,{title:"As it happened",intro:"The log, newest first — the query exactly as it was typed, and who typed it. This is the one to read when somebody says search is not finding something.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"When"}),e.jsx("th",{children:"Viewer"}),e.jsx("th",{children:"Query"})]})}),e.jsx("tbody",{children:r.length===0?e.jsx(J,{columns:3,children:"No searches in this window."}):r.map((c,o)=>e.jsxs("tr",{children:[e.jsx("td",{className:"muted nowrap",children:F(c.occurredAt)}),e.jsx("td",{children:c.username||e.jsx(N,{tone:"warn",children:c.userId||"unknown"})}),e.jsx("td",{children:c.query})]},`${c.occurredAt}:${o}`))})]})})})]})]})}function un(s,n){if(n===0)return s>0?"new this week":"no change";const t=Math.round((s-n)/n*100);return`${t>0?"+":""}${t}% vs last week`}function $a(){const{data:s,error:n,loading:t}=Y("/admin/api/views",{pollMs:6e4}),a=(s==null?void 0:s.daily)??[],i=(s==null?void 0:s.hourly)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Views",intro:"How often people reach Memby’s home screen. This measures app use, not playback streams."}),e.jsx(P,{message:n}),t?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[e.jsx(ae,{tiles:[{label:un((s==null?void 0:s.today.visits)??0,(s==null?void 0:s.lastWeek.visits)??0),value:w(s==null?void 0:s.today.visits),icon:"overview",tone:"data"},{label:un((s==null?void 0:s.today.viewers)??0,(s==null?void 0:s.lastWeek.viewers)??0),value:w(s==null?void 0:s.today.viewers),icon:"people",tone:"note"},{label:"busiest time today",value:(s==null?void 0:s.busiestHour)||"—",small:!0,icon:"clock",tone:"info"}]}),e.jsx(M,{title:"Visits by day",intro:"One visit is a signed-in home-screen opening. Viewers are distinct signed-in profiles.",icon:"chart",tone:"data",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Day"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:a.length===0?e.jsx(J,{columns:3,children:"No home-screen visits yet."}):a.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:w(l.visits)}),e.jsx("td",{className:"num",children:w(l.viewers)})]},l.label))})]})})}),e.jsx(M,{title:"Today by hour",intro:"Local New Zealand time. Use this to see when viewers are opening Memby.",icon:"clock",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Hour"}),e.jsx("th",{className:"num",children:"Visits"}),e.jsx("th",{className:"num",children:"Viewers"})]})}),e.jsx("tbody",{children:i.length===0?e.jsx(J,{columns:3,children:"No home-screen visits yet today."}):i.map(l=>e.jsxs("tr",{children:[e.jsx("td",{children:l.label}),e.jsx("td",{className:"num",children:w(l.visits)}),e.jsx("td",{className:"num",children:w(l.viewers)})]},l.label))})]})})})]})]})}const Ra=s=>s.mediaType==="episode"?`${s.seriesTitle} S${String(s.seasonNumber).padStart(2,"0")}E${String(s.episodeNumber).padStart(2,"0")}`:s.title;function Ea(){var i;const s=Y("/admin/api/media-reports",{pollMs:15e3}),{busy:n,run:t}=se(),a=(l,r)=>t(`${l.id}-${r}`,async()=>{await q.post(`/admin/api/media-reports/${l.id}/status`,{status:r}),await s.reload()});return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Media reports",intro:"Viewer-reported problems and the individual replacement searches they asked Memby to start."}),s.loading?e.jsx(B,{rows:4}):e.jsx(M,{title:"Open and recent reports",intro:"A replacement always targets one film or one episode. Existing files stay in place while Radarr or Sonarr applies its normal import policy.",icon:"inbox",children:(i=s.data)!=null&&i.reports.length?e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Media"}),e.jsx("th",{children:"Report"}),e.jsx("th",{children:"Viewer"}),e.jsx("th",{children:"Replacement"}),e.jsx("th",{children:"Status"}),e.jsx("th",{children:"Reported"}),e.jsx("th",{children:"Actions"})]})}),e.jsx("tbody",{children:s.data.reports.map(l=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("b",{children:Ra(l)}),e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.title})]}),e.jsxs("td",{children:[l.reason.replaceAll("_"," "),l.comment?e.jsxs(e.Fragment,{children:[e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.comment})]}):null]}),e.jsxs("td",{children:[l.reportedByUsername,e.jsx("br",{}),e.jsx("span",{className:"muted",children:l.reportedByDevice||"Unknown device"})]}),e.jsx("td",{children:e.jsx(N,{tone:l.replacementRequested?"note":void 0,children:l.replacementRequested?l.replacementStatus||"Requested":"Not requested"})}),e.jsx("td",{children:e.jsx(N,{tone:l.status==="resolved"?"ok":l.status==="dismissed"?void 0:"warn",children:l.status})}),e.jsx("td",{className:"nowrap muted",children:F(l.createdAt)}),e.jsxs("td",{children:[e.jsx(T,{size:"sm",variant:"quiet",busy:n===`${l.id}-acknowledged`,onClick:()=>void a(l,"acknowledged"),children:"Acknowledge"})," ",e.jsx(T,{size:"sm",variant:"quiet",busy:n===`${l.id}-resolved`,onClick:()=>void a(l,"resolved"),children:"Resolve"})," ",e.jsx(T,{size:"sm",variant:"quiet",busy:n===`${l.id}-dismissed`,onClick:()=>void a(l,"dismissed"),children:"Dismiss"})]})]},l.id))})]})}):e.jsx(ee,{children:"No media problems have been reported."})})]})}const mn={user:"",kind:"",channel:"",status:"",source:"",q:"",from:"",to:""},Aa=[{value:1,label:"Today"},{value:7,label:"7 days"},{value:30,label:"30 days"},{value:90,label:"90 days"}],Ke=100,On={sent:"ok",delivered:"ok",failed:"bad",pending:"warn",skipped:"idle"},Ia={"in-app":"note",broadcast:"info",webhook:"data"},os={"in-app":"In-app",broadcast:"Broadcast",webhook:"Webhook"};function Se(s){if(!s)return"—";const n=s.replace(/[-_.:]+/g," ").trim();return n.charAt(0).toUpperCase()+n.slice(1)}function is(s,n){return(s??[]).map(t=>e.jsxs("option",{value:t.value,children:[n(t.value)," (",t.count,")"]},t.value))}function La(){var y,v,x,$;const[s,n]=f.useState(7),[t,a]=f.useState(mn),[i,l]=f.useState(0),[r,d]=f.useState(null),c=f.useMemo(()=>Ve({...t,days:t.from?void 0:s,limit:Ke,offset:i*Ke}),[t,s,i]),o=Y(`/admin/api/notification-log${c}`),u=o.data,j=k=>{a(R=>({...R,...k})),l(0),d(null)},p=Object.values(t).some(k=>k!==""),g=u==null?void 0:u.totals,b=(u==null?void 0:u.retentionDays)??90,h=(u==null?void 0:u.entries.length)??0,m=!u||u.total===0?0:i*Ke+1;return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"Notifications",intro:"Everything Memby sent — a viewer's own news, the bar every television draws, and each outbound webhook — with what became of it. Every feature reports through one notification service, so this is the whole trail rather than whichever half a feature remembered to log.",actions:e.jsx(G,{className:"btn",to:"/admin/notification-settings",children:"TV notification settings"})}),e.jsx(P,{message:o.error}),g?e.jsx(ae,{tiles:[{label:"Sent",value:w(g.sent),icon:"send",tone:"ok"},{label:"Confirmed",value:w(g.delivered),icon:"check",tone:"ok"},{label:"Failed",value:w(g.failed),icon:"alert",tone:g.failed>0?"bad":void 0},{label:"Skipped",value:w(g.skipped),icon:"filter",tone:"idle"},{label:"People reached",value:w(g.users),icon:"people",tone:"note"},{label:"History kept",value:`${b} days`,small:!0,icon:"clock"}]}):null,e.jsxs("div",{className:"filters",children:[e.jsx(D,{label:"Window",children:e.jsx(Ue,{value:t.from?-1:s,options:Aa.map(k=>({value:k.value,label:k.label})),onChange:k=>{n(k),j({from:"",to:""})}})}),e.jsx(D,{label:"Person",children:e.jsxs("select",{value:t.user,onChange:k=>j({user:k.target.value}),children:[e.jsx("option",{value:"",children:"Anyone"}),((u==null?void 0:u.users)??[]).map(k=>e.jsx("option",{value:k.id,children:k.username||k.id},k.id))]})}),e.jsx(D,{label:"Type",children:e.jsxs("select",{value:t.kind,onChange:k=>j({kind:k.target.value}),children:[e.jsx("option",{value:"",children:"Any type"}),is((y=u==null?void 0:u.facets)==null?void 0:y.kinds,Se)]})}),e.jsx(D,{label:"Channel",children:e.jsxs("select",{value:t.channel,onChange:k=>j({channel:k.target.value}),children:[e.jsx("option",{value:"",children:"Any channel"}),is((v=u==null?void 0:u.facets)==null?void 0:v.channels,k=>os[k]??Se(k))]})}),e.jsx(D,{label:"Status",children:e.jsxs("select",{value:t.status,onChange:k=>j({status:k.target.value}),children:[e.jsx("option",{value:"",children:"Any status"}),is((x=u==null?void 0:u.facets)==null?void 0:x.statuses,Se)]})}),e.jsx(D,{label:"Source",children:e.jsxs("select",{value:t.source,onChange:k=>j({source:k.target.value}),children:[e.jsx("option",{value:"",children:"Any service"}),is(($=u==null?void 0:u.facets)==null?void 0:$.sources,Se)]})}),e.jsx(D,{label:"From",children:e.jsx("input",{type:"date",value:t.from,onChange:k=>j({from:k.target.value})})}),e.jsx(D,{label:"To",children:e.jsx("input",{type:"date",value:t.to,onChange:k=>j({to:k.target.value})})}),e.jsx(D,{label:"Search",grow:!0,children:e.jsx("input",{type:"search",value:t.q,placeholder:"Title, message, error or person",onChange:k=>j({q:k.target.value})})}),e.jsx("div",{className:"filter-actions",children:p?e.jsx(T,{variant:"quiet",size:"sm",onClick:()=>{a(mn),l(0),d(null)},children:"Clear"}):null})]}),o.loading&&!u?e.jsx(B,{}):null,u&&u.days.length>1?e.jsx(M,{title:"Notifications per day",intro:"Failures and deliberate skips are counted beside the deliveries, because a quiet week and a week nothing was allowed to send look identical otherwise.",icon:"chart",tone:"info",children:e.jsx(hs,{data:u.days,labelOf:k=>k.day,valueOf:k=>k.sent+k.delivered+k.failed+k.skipped,toneOf:k=>k.failed>0?"bad":void 0,title:k=>`${k.day}: ${k.sent+k.delivered} sent, ${k.failed} failed, ${k.skipped} skipped`})}):null,u?e.jsx(M,{title:"History",intro:"Newest first. A row says who, what and whether it worked on its own; open one for the whole message and the delivery response.",icon:"send",tone:"ok",actions:e.jsx("span",{className:"filter-summary",children:u.total===0?"nothing matches":`${w(m)}–${w(m+h-1)} of ${w(u.total)}`}),footer:u.total>Ke?e.jsxs(e.Fragment,{children:[e.jsx(T,{size:"sm",disabled:i===0,onClick:()=>l(i-1),children:"Newer"}),e.jsx(T,{size:"sm",disabled:(i+1)*Ke>=u.total,onClick:()=>l(i+1),children:"Older"})]}):void 0,children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{className:"nowrap",children:"When"}),e.jsx("th",{children:"Recipient"}),e.jsx("th",{children:"Type"}),e.jsx("th",{children:"Title"}),e.jsx("th",{children:"Channel"}),e.jsx("th",{children:"Source"}),e.jsx("th",{children:"Status"}),e.jsx("th",{"aria-label":"Details"})]})}),e.jsx("tbody",{children:u.entries.length===0?e.jsx(J,{columns:8,children:"No notifications match these filters."}):u.entries.map(k=>e.jsxs(f.Fragment,{children:[e.jsx(Da,{entry:k,open:r===k.id,onToggle:()=>d(r===k.id?null:k.id)}),r===k.id?e.jsx(qa,{entry:k}):null]},k.id))})]})})}):null]})}function Da({entry:s,open:n,onToggle:t}){return e.jsxs("tr",{"data-selected":n||void 0,children:[e.jsx("td",{className:"nowrap muted",children:F(s.occurredAt)}),e.jsx("td",{children:s.userId?e.jsx(G,{className:"table-row-link",to:`/admin/accounts/${encodeURIComponent(s.userId)}`,children:s.username||s.userId}):s.target?e.jsx("span",{className:"muted",children:s.target}):e.jsx("span",{className:"quiet",children:"Everyone"})}),e.jsx("td",{className:"mono",children:s.kind||"—"}),e.jsx("td",{children:s.title||e.jsx("span",{className:"quiet",children:"—"})}),e.jsx("td",{className:"nowrap",children:e.jsx(N,{tone:Ia[s.channel],children:os[s.channel]??s.channel})}),e.jsx("td",{className:"muted",children:Se(s.source)}),e.jsx("td",{className:"nowrap",children:e.jsx(N,{tone:On[s.status]??"idle",children:s.status})}),e.jsx("td",{className:"nowrap",children:e.jsx(T,{size:"sm",variant:"quiet",onClick:t,icon:n?"close":"list",children:n?"Close":"Details"})})]})}function qa({entry:s}){const n=Object.entries(s.metadata??{}).filter(([,t])=>t!=null&&String(t)!=="");return e.jsx("tr",{className:"detail-row",children:e.jsx("td",{colSpan:8,children:e.jsxs("section",{className:"logdrawer","aria-label":`Notification ${s.id}`,children:[e.jsxs("header",{className:"logdrawer-head",children:[e.jsxs("div",{children:[e.jsxs("p",{className:"logdrawer-place",children:[e.jsx("span",{children:os[s.channel]??s.channel}),e.jsx("span",{className:"logrow-sep","aria-hidden":"true",children:"›"}),Se(s.source)]}),e.jsx("b",{children:s.title||Se(s.kind)}),s.detail?e.jsx("p",{className:"logdrawer-error",children:s.detail}):null]}),e.jsx("div",{className:"logdrawer-actions",children:e.jsx(N,{tone:On[s.status]??"idle",children:s.status})})]}),e.jsxs("div",{className:"logdrawer-grid",children:[e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Delivery"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Sent"}),e.jsx("dd",{children:F(s.occurredAt)}),s.eventAt?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Event"}),e.jsx("dd",{children:F(s.eventAt)})]}):null,e.jsx("dt",{children:"Channel"}),e.jsx("dd",{children:os[s.channel]??s.channel}),e.jsx("dt",{children:"Type"}),e.jsx("dd",{children:s.kind||"—"}),e.jsx("dt",{children:"Service"}),e.jsx("dd",{children:Se(s.source)}),e.jsx("dt",{children:"Took"}),e.jsxs("dd",{children:[s.durationMs,"ms"]})]})]}),e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Recipient"}),e.jsxs("dl",{children:[e.jsx("dt",{children:"Person"}),e.jsx("dd",{children:s.userId?s.username||s.userId:"the whole household"}),s.target?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Destination"}),e.jsx("dd",{children:s.target})]}):null,s.itemId?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Title id"}),e.jsx("dd",{children:s.itemId})]}):null,s.sourceKey?e.jsxs(e.Fragment,{children:[e.jsx("dt",{children:"Source key"}),e.jsx("dd",{children:s.sourceKey})]}):null]})]}),n.length?e.jsxs("div",{className:"logdrawer-section",children:[e.jsx("h4",{children:"Context"}),e.jsx("dl",{children:n.map(([t,a])=>e.jsxs(f.Fragment,{children:[e.jsx("dt",{children:Se(t)}),e.jsx("dd",{children:String(a)})]},t))})]}):null]}),s.body?e.jsxs("div",{className:"logdrawer-raw",children:[e.jsx("h4",{children:"Message"}),e.jsx("p",{className:"notification-body",children:s.body})]}):null]})})})}const pn=[{value:"everywhere",label:"Everywhere",description:"Show informational banners over the home screen and active playback."},{value:"home_only",label:"Home only",description:"Show banners on the launcher, without interrupting a film or programme."},{value:"off",label:"Off",description:"Do not show informational banners on any television."}];function Fa(){var g,b;const s=Y("/admin/api/gateway-settings"),n=Y("/admin/api/accounts"),{busy:t,run:a}=se(),{wrap:i}=te(),[l,r]=f.useState(null),[d,c]=f.useState({});f.useEffect(()=>{l===null&&s.data&&r(s.data.settings.notificationDisplay||"home_only")},[l,s.data]),f.useEffect(()=>{const h=n.data;h&&c(m=>{const y={...m};for(const v of h.accounts??[])y[v.id]||(y[v.id]={...v.notifications});return y})},[n.data]);const o=()=>a("display",async()=>{if(!s.data||l===null)return;const h=await i(async()=>{const m=await q.get("/admin/api/gateway-settings");return q.post("/admin/api/gateway-settings",{...m.settings,notificationDisplay:l})},"Television notification display saved.");h&&(s.set(h),r(h.settings.notificationDisplay))}),u=h=>a(`account-${h.id}`,async()=>{var v;const m=d[h.id];if(!m)return;const y=await i(()=>q.put(`/admin/api/accounts/${encodeURIComponent(h.id)}/notifications`,m),`Notification settings saved for ${h.shortName||h.username}.`);y&&(c(x=>({...x,[h.id]:y})),n.set({accounts:(((v=n.data)==null?void 0:v.accounts)??[]).map(x=>x.id===h.id?{...x,notifications:y}:x)}))}),j=s.error||n.error,p=((g=n.data)==null?void 0:g.accounts)??[];return e.jsxs(e.Fragment,{children:[e.jsx(U,{title:"TV notifications",intro:"Choose where household notices may appear, then mute or allow them for each person.",actions:e.jsx(G,{className:"btn",to:"/admin/notifications",children:"View notification history"})}),e.jsx(P,{message:j}),s.loading||l===null?e.jsx(B,{rows:2}):e.jsx(M,{title:"Where notifications appear",intro:"This household-wide rule applies to every user and television. Maintenance and mandatory update screens still appear because they are not optional notifications.",icon:"tv",tone:"info",actions:e.jsx(N,{tone:l==="off"?void 0:"ok",children:((b=pn.find(h=>h.value===l))==null?void 0:b.label)??"Home only"}),footer:e.jsx(T,{variant:"primary",icon:"check",busy:t==="display",onClick:()=>void o(),children:"Save display rule"}),children:e.jsx("div",{className:"checks columns",children:pn.map(h=>e.jsxs("label",{className:"check",children:[e.jsx("input",{type:"radio",name:"notification-display",checked:l===h.value,onChange:()=>r(h.value)}),e.jsx("span",{className:"switch","aria-hidden":"true"}),e.jsxs("span",{className:"check-body",children:[e.jsx("b",{children:h.label}),e.jsx("p",{children:h.description})]})]},h.value))})}),e.jsx(M,{title:"People",intro:"A person's master switch follows them to every television. Open their user page to choose individual notification types.",icon:"people",tone:"note",children:n.loading?e.jsx(B,{}):p.length===0?e.jsx("p",{className:"empty",children:"No one has signed in to Memby yet."}):e.jsx(ue,{cols:"2",children:p.map(h=>{const m=d[h.id]??h.notifications,y=h.shortName||h.username||"Unnamed user",v=m.enabled!==h.notifications.enabled;return e.jsx(M,{title:y,intro:h.shortName?h.username:"Memby user",icon:"person",tone:"note",actions:e.jsx(N,{tone:m.enabled?"ok":void 0,children:m.enabled?"enabled":"muted"}),footer:e.jsxs(e.Fragment,{children:[e.jsx(T,{variant:"primary",busy:t===`account-${h.id}`,disabled:!v,onClick:()=>void u(h),children:"Save"}),e.jsx(G,{className:"btn",to:`/admin/accounts/${encodeURIComponent(h.id)}`,children:"Choose notification types"})]}),children:e.jsx(Z,{label:"All notifications",hint:"Turn off every optional notification for this person.",checked:m.enabled,onChange:x=>c($=>({...$,[h.id]:{...m,enabled:x}}))})},h.id)})})})]})}const Oa=s=>s==="detected"?"ok":s==="failed"?"bad":s==="no_match"?"warn":"info",Pa=s=>s==="no_match"?"no match":s,xn=s=>({"live-playback":"Live playback","tracearr-next":"Next episode","tracearr-binge-prefetch":"Binge look-ahead","multi-user-demand":"Multiple viewers"}[s]??s)||"Unknown",jn=(s,n)=>s>0&&n>0?`S${String(s).padStart(2,"0")}E${String(n).padStart(2,"0")}`:"Episode";function _a(){var h,m,y,v;const s=Y("/admin/api/credits?limit=150",{pollMs:15e3}),{wrap:n}=te(),{busy:t,run:a}=se(),[i,l]=f.useState(),[r,d]=f.useState(!1);f.useEffect(()=>{!r&&s.data&&l(s.data.settings)},[s.data,r]);const c=x=>{l($=>$&&{...$,...x}),d(!0)},o=()=>{i&&a("save",async()=>{const x=await n(()=>q.put("/admin/api/credits",i),"Credits scanning settings saved.");x&&(s.set(x),l(x.settings),d(!1))})},u=((h=s.data)==null?void 0:h.history)??[],j=((m=s.data)==null?void 0:m.pending)??[],p=u.filter(x=>x.outcome==="detected").length,g=u.filter(x=>x.outcome==="no_match").length,b=u.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(P,{message:s.error}),s.loading||!i?e.jsx(B,{}):e.jsxs(e.Fragment,{children:[(y=s.data)!=null&&y.enabled?null:e.jsx(xe,{tone:"warn",children:"Credits detection is disabled in the gateway environment. These settings will be retained for the next time it is enabled."}),e.jsx(ae,{tiles:[{label:"Waiting candidates",value:w((v=s.data)==null?void 0:v.queueDepth),icon:"clock",tone:j.length?"info":void 0},{label:"Detected in this history",value:w(p),icon:"check",tone:"ok"},{label:"No match",value:w(g),icon:"search",tone:g?"warn":void 0},{label:"Failed",value:w(b),icon:"alert",tone:b?"bad":void 0}]}),e.jsxs(ue,{cols:"wide",children:[e.jsx(M,{title:"Candidate controls",intro:"The worker remains single-file and scans one episode at a time. These values control what is allowed to wait and how far prediction looks ahead.",icon:"sliders",tone:"note",footer:e.jsx(T,{variant:"primary",icon:"check",busy:t==="save",disabled:!r,onClick:o,children:"Save settings"}),children:e.jsxs("div",{className:"fields",children:[e.jsx(D,{label:"Candidate limit",hint:"Maximum episodes waiting in the priority queue. Stronger candidates displace weaker ones when it is full.",children:e.jsx("input",{type:"number",min:1,max:100,value:i.candidateLimit,onChange:x=>c({candidateLimit:Number(x.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:i.prefetchEpisodes,onChange:x=>c({prefetchEpisodes:Number(x.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:i.prefetchEpisodes,max:20,value:i.maxPrefetch,onChange:x=>c({maxPrefetch:Number(x.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:i.retryHours,onChange:x=>c({retryHours:Number(x.target.value)})})})]})}),e.jsxs(M,{title:"How selection works",icon:"sparkle",tone:"data",children:[e.jsx("p",{className:"muted",children:"Recent viewing predicts the next few episodes. Priority favours a programme playing now, then the next episode, fast viewing, and episodes several people are approaching."}),e.jsx("p",{className:"muted",children:"A completed speculative attempt enters the retry delay even when no marker was found. Live playback can still raise an immediate candidate because somebody is waiting for it."})]})]}),e.jsx(M,{title:"Waiting candidates",intro:"The exact worker order after marker checks and retry cooldowns. A refresh may replace this list as viewing changes.",icon:"list",tone:"info",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Episode"}),e.jsx("th",{children:"Reason"}),e.jsx("th",{className:"num",children:"Priority"}),e.jsx("th",{className:"num",children:"Viewers"}),e.jsx("th",{children:"Demand seen"}),e.jsx("th",{children:"Item ID"})]})}),e.jsx("tbody",{children:j.length===0?e.jsx(J,{columns:6,children:"No episodes are waiting to be scanned."}):j.map(x=>e.jsxs("tr",{children:[e.jsx("td",{children:jn(x.season,x.episode)}),e.jsx("td",{children:e.jsx(N,{tone:"info",children:xn(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:F(x.lastViewed),children:fe(x.lastViewed)}),e.jsx("td",{className:"mono muted",children:x.itemId})]},x.itemId))})]})})}),e.jsx(M,{title:"Scan history",intro:"Completed worker attempts, newest first. Repeated item IDs make an ineffective retry delay visible immediately.",icon:"history",tone:"note",children:e.jsx(K,{children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:"Finished"}),e.jsx("th",{children:"Programme"}),e.jsx("th",{children:"Selected because"}),e.jsx("th",{children:"Result"}),e.jsx("th",{children:"Marker"}),e.jsx("th",{children:"Evidence"}),e.jsx("th",{className:"num",children:"Took"})]})}),e.jsx("tbody",{children:u.length===0?e.jsx(J,{columns:7,children:"No credits scans have completed yet."}):u.map(x=>e.jsxs("tr",{children:[e.jsx("td",{className:"nowrap muted",title:F(x.finishedAt),children:fe(x.finishedAt)}),e.jsxs("td",{children:[e.jsx("b",{children:x.seriesName||x.itemName||x.itemId}),e.jsxs("span",{className:"table-sub",children:[jn(x.season,x.episode),x.itemName&&x.seriesName?` · ${x.itemName}`:""]})]}),e.jsxs("td",{children:[e.jsx(N,{tone:"info",children:xn(x.reason)}),e.jsxs("span",{className:"table-sub",children:["priority ",x.priority]})]}),e.jsxs("td",{children:[e.jsx(N,{tone:Oa(x.outcome),children:Pa(x.outcome)}),x.error?e.jsx("span",{className:"table-sub",children:x.error}):null]}),e.jsx("td",{className:"nowrap",children:x.markerMs>0?ye(x.markerMs):"—"}),e.jsxs("td",{className:"muted",children:[x.method||"visual",x.confidence>0?` · ${as(x.confidence)}`:"",x.frames>0?` · ${x.frames} frames`:""]}),e.jsx("td",{className:"num muted",children:ye(x.durationMs)})]},x.id))})]})})})]})]})}function Ua(){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 Wa(){return e.jsx(Wn,{children:e.jsx(nt,{children:e.jsx(lt,{children:e.jsx(mt,{children:e.jsx(Bn,{children:e.jsxs(W,{path:"/admin",element:e.jsx(ht,{}),children:[e.jsx(W,{index:!0,element:e.jsx(xt,{})}),e.jsx(W,{path:"activity",element:e.jsx(gt,{})}),e.jsx(W,{path:"accounts",element:e.jsx(vt,{})}),e.jsx(W,{path:"accounts/:userId",element:e.jsx(ft,{})}),e.jsx(W,{path:"accounts/:userId/settings",element:e.jsx(Ct,{})}),e.jsx(W,{path:"clients",element:e.jsx(Tt,{})}),e.jsx(W,{path:"logins",element:e.jsx(Rt,{})}),e.jsx(W,{path:"devices/:deviceId",element:e.jsx(Lt,{})}),e.jsx(W,{path:"library",element:e.jsx(Dt,{})}),e.jsx(W,{path:"requests",element:e.jsx(qt,{})}),e.jsx(W,{path:"recommendations",element:e.jsx(Ft,{})}),e.jsx(W,{path:"inspector",element:e.jsx(_t,{})}),e.jsx(W,{path:"hero",element:e.jsx(Vt,{})}),e.jsx(W,{path:"metadata-hero",element:e.jsx(zt,{})}),e.jsx(W,{path:"features",element:e.jsx(Kt,{})}),e.jsx(W,{path:"playback",element:e.jsx(Gt,{})}),e.jsx(W,{path:"subtitles",element:e.jsx(Zt,{})}),e.jsx(W,{path:"credits",element:e.jsx(_a,{})}),e.jsx(W,{path:"updates",element:e.jsx(Yt,{})}),e.jsx(W,{path:"tasks",element:e.jsx(ti,{})}),e.jsx(W,{path:"tasks/:taskId",element:e.jsx(oi,{})}),e.jsx(W,{path:"integrations",element:e.jsx(pi,{})}),e.jsx(W,{path:"integrations/webhooks",element:e.jsx(fi,{})}),e.jsx(W,{path:"integrations/:integrationId",element:e.jsx(Ci,{})}),e.jsx(W,{path:"maintenance",element:e.jsx($i,{})}),e.jsx(W,{path:"runtime",element:e.jsx(Ri,{})}),e.jsx(W,{path:"settings",element:e.jsx(Li,{})}),e.jsx(W,{path:"imports",element:e.jsx(Oi,{})}),e.jsx(W,{path:"logs",element:e.jsx(ba,{})}),e.jsx(W,{path:"journeys",element:e.jsx(ya,{})}),e.jsx(W,{path:"journeys/:userId",element:e.jsx(Ca,{})}),e.jsx(W,{path:"views",element:e.jsx($a,{})}),e.jsx(W,{path:"engagement",element:e.jsx(Ma,{})}),e.jsx(W,{path:"searches",element:e.jsx(Ta,{})}),e.jsx(W,{path:"media-reports",element:e.jsx(Ea,{})}),e.jsx(W,{path:"notifications",element:e.jsx(La,{})}),e.jsx(W,{path:"notification-settings",element:e.jsx(Fa,{})}),e.jsx(W,{path:"overview",element:e.jsx(Es,{to:"/admin",replace:!0})}),e.jsx(W,{path:"ratings",element:e.jsx(Es,{to:"/admin/integrations/mdblist",replace:!0})}),e.jsx(W,{path:"*",element:e.jsx(Ua,{})})]})})})})})})}const Pn=document.getElementById("root");if(!Pn)throw new Error("the console has no root element to render into");bn(Pn).render(e.jsx(f.StrictMode,{children:e.jsx(Wa,{})})); diff --git a/admin-ui/dist/index.html b/admin-ui/dist/index.html index 6aa6ced..04f1d83 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/App.tsx b/admin-ui/src/App.tsx index 41f5471..62ac5cf 100644 --- a/admin-ui/src/App.tsx +++ b/admin-ui/src/App.tsx @@ -4,6 +4,7 @@ import { GatewayProvider } from './lib/gateway'; import { NotificationProvider } from './lib/notifications'; import { ToastProvider } from './lib/toast'; import { PageHead } from './components/ui'; +import { ReconnectOverlay } from './components/ReconnectOverlay'; import { OverviewPage } from './pages/Overview'; import { ActivityPage } from './pages/Activity'; @@ -125,6 +126,7 @@ export function App() { } /> + diff --git a/admin-ui/src/api/client.ts b/admin-ui/src/api/client.ts index 5bdbfa3..e2c972e 100644 --- a/admin-ui/src/api/client.ts +++ b/admin-ui/src/api/client.ts @@ -7,17 +7,28 @@ * by reloading into the gateway's own sign-in form rather than by anything here. */ +import { reportTemporaryAvailabilityFailure } from '../lib/availability'; + /** ApiError carries the status so a caller can tell "not configured" (404) from "broken". */ export class ApiError extends Error { constructor( message: string, readonly status: number, + readonly temporaryAvailability = false, ) { super(message); this.name = 'ApiError'; } } +function isTemporaryStatus(status: number): boolean { + return status === 502 || status === 503 || status === 504; +} + +export function isTemporaryAvailabilityError(error: unknown): boolean { + return error instanceof ApiError && error.temporaryAvailability; +} + /* The sign-in behind this console lasts twelve hours and slides forward only for requests an operator actually caused, so the poll of a tab nobody is reading cannot keep it alive. Anything the console does while somebody is working it says so with this header; @@ -48,14 +59,25 @@ function reauthenticate(): boolean { } async function request(path: string, options: RequestInit = {}): Promise { - const response = await fetch(path, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...(operatorPresent() ? { 'X-Memby-Admin-Active': '1' } : {}), - ...(options.headers ?? {}), - }, - }); + let response: Response; + try { + response = await fetch(path, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...(operatorPresent() ? { 'X-Memby-Admin-Active': '1' } : {}), + ...(options.headers ?? {}), + }, + }); + } catch (error) { + // Fetch reports a refused connection, DNS failure, or broken proxy as TypeError. + // Keep that implementation detail out of the UI and begin the one global recovery loop. + if (error instanceof TypeError) { + reportTemporaryAvailabilityFailure(); + throw new ApiError('The server is temporarily unavailable.', 0, true); + } + throw error; + } if (response.status === 401) { throw new ApiError( reauthenticate() @@ -65,6 +87,10 @@ async function request(path: string, options: RequestInit = {}): Promise { ); } if (!response.ok) { + if (isTemporaryStatus(response.status)) { + reportTemporaryAvailabilityFailure(); + throw new ApiError('The server is temporarily unavailable.', response.status, true); + } const body = (await response.json().catch(() => ({}))) as { error?: string }; throw new ApiError(body.error ?? `Request failed (${response.status})`, response.status); } diff --git a/admin-ui/src/components/ReconnectOverlay.tsx b/admin-ui/src/components/ReconnectOverlay.tsx new file mode 100644 index 0000000..1fe9dc5 --- /dev/null +++ b/admin-ui/src/components/ReconnectOverlay.tsx @@ -0,0 +1,17 @@ +import { useAvailability } from '../lib/availability'; + +export function ReconnectOverlay() { + const { reconnecting, attempt } = useAvailability(); + if (!reconnecting) return null; + + return ( +
+
+
+
+ ); +} diff --git a/admin-ui/src/lib/availability.ts b/admin-ui/src/lib/availability.ts new file mode 100644 index 0000000..e08b9d3 --- /dev/null +++ b/admin-ui/src/lib/availability.ts @@ -0,0 +1,90 @@ +import { useEffect, useState } from 'react'; + +export interface AvailabilityState { + reconnecting: boolean; + attempt: number; +} + +type StateListener = (state: AvailabilityState) => void; +type RecoveryListener = () => void; + +const RETRY_MS = 2500; +const HEALTH_TIMEOUT_MS = 2000; +const listeners = new Set(); +const recoveryListeners = new Set(); +let state: AvailabilityState = { reconnecting: false, attempt: 0 }; +let loopRunning = false; +let retryTimer: number | undefined; +let healthRequest: AbortController | undefined; + +function publish(next: AvailabilityState) { + state = next; + listeners.forEach((listener) => listener(state)); +} + +function waitForRetry(): Promise { + return new Promise((resolve) => { + retryTimer = window.setTimeout(() => { + retryTimer = undefined; + resolve(); + }, RETRY_MS); + }); +} + +async function reconnect() { + if (loopRunning) return; + loopRunning = true; + try { + while (state.reconnecting) { + const attempt = state.attempt + 1; + publish({ reconnecting: true, attempt }); + healthRequest = new AbortController(); + const timeout = window.setTimeout(() => healthRequest?.abort(), HEALTH_TIMEOUT_MS); + try { + const response = await fetch('/healthz', { + cache: 'no-store', + credentials: 'same-origin', + signal: healthRequest.signal, + }); + if (response.ok) { + publish({ reconnecting: false, attempt: 0 }); + recoveryListeners.forEach((listener) => listener()); + break; + } + } catch { + // The next attempt is the recovery path. Health failures are deliberately silent. + } finally { + window.clearTimeout(timeout); + healthRequest = undefined; + } + if (state.reconnecting) await waitForRetry(); + } + } finally { + loopRunning = false; + window.clearTimeout(retryTimer); + retryTimer = undefined; + healthRequest = undefined; + } +} + +export function reportTemporaryAvailabilityFailure(): void { + if (!state.reconnecting) publish({ reconnecting: true, attempt: 0 }); + void reconnect(); +} + +export function subscribeAvailability(listener: StateListener): () => void { + listeners.add(listener); + listener(state); + return () => listeners.delete(listener); +} + +export function subscribeToRecovery(listener: RecoveryListener): () => void { + recoveryListeners.add(listener); + return () => recoveryListeners.delete(listener); +} + +export function useAvailability(): AvailabilityState { + const [current, setCurrent] = useState(state); + useEffect(() => subscribeAvailability(setCurrent), []); + return current; +} diff --git a/admin-ui/src/lib/gateway.tsx b/admin-ui/src/lib/gateway.tsx index db71847..81acc57 100644 --- a/admin-ui/src/lib/gateway.tsx +++ b/admin-ui/src/lib/gateway.tsx @@ -10,6 +10,7 @@ import { } from 'react'; import { api } from '../api/client'; import type { AdminStatus } from '../api/types'; +import { subscribeToRecovery } from './availability'; /* /admin/api/status is the console's shared heartbeat. * @@ -101,6 +102,8 @@ export function GatewayProvider({ children }: { children: ReactNode }) { }; }, [reload]); + useEffect(() => subscribeToRecovery(() => void reload()), [reload]); + const value = useMemo( () => ({ status, diff --git a/admin-ui/src/lib/hooks.ts b/admin-ui/src/lib/hooks.ts index 7e8a214..2821423 100644 --- a/admin-ui/src/lib/hooks.ts +++ b/admin-ui/src/lib/hooks.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api } from '../api/client'; +import { subscribeToRecovery } from './availability'; /* The two hooks every page is built from. * @@ -77,6 +78,8 @@ export function useQuery(path: string, options: QueryOptions = {}): Loadable< }; }, [run]); + useEffect(() => subscribeToRecovery(() => void run()), [run]); + useEffect(() => { if (!pollMs || !enabled) return; let timer: number | undefined; diff --git a/admin-ui/src/lib/notifications.tsx b/admin-ui/src/lib/notifications.tsx index 9f14b50..979f676 100644 --- a/admin-ui/src/lib/notifications.tsx +++ b/admin-ui/src/lib/notifications.tsx @@ -11,6 +11,7 @@ import { import { api } from '../api/client'; import type { Tone } from './format'; import type { IconName } from '../components/Icon'; +import { subscribeToRecovery } from './availability'; /* The administrative feed, shared by the bell in the top bar and the activity page. * @@ -117,6 +118,8 @@ export function NotificationProvider({ children }: { children: ReactNode }) { void reload(); }, [reload]); + useEffect(() => subscribeToRecovery(() => void reload()), [reload]); + useEffect(() => { let source: EventSource | null = null; let fallback: number | undefined; diff --git a/admin-ui/src/lib/toast.tsx b/admin-ui/src/lib/toast.tsx index dd64061..37578b6 100644 --- a/admin-ui/src/lib/toast.tsx +++ b/admin-ui/src/lib/toast.tsx @@ -1,6 +1,7 @@ import { createContext, useCallback, useContext, useMemo, useRef, useState, type ReactNode } from 'react'; import { Icon } from '../components/Icon'; import type { Tone } from './format'; +import { isTemporaryAvailabilityError } from '../api/client'; /* Toasts say what a mutation did. * @@ -51,6 +52,7 @@ export function ToastProvider({ children }: { children: ReactNode }) { if (success) show(success, 'ok'); return result; } catch (err) { + if (isTemporaryAvailabilityError(err)) return undefined; show(err instanceof Error ? err.message : String(err), 'bad'); return undefined; } diff --git a/admin-ui/src/pages/Accounts.tsx b/admin-ui/src/pages/Accounts.tsx index c366847..6c1cb78 100644 --- a/admin-ui/src/pages/Accounts.tsx +++ b/admin-ui/src/pages/Accounts.tsx @@ -1,216 +1,79 @@ +import { useState } from 'react'; import { Link } from 'react-router-dom'; -import { useQuery } from '../lib/hooks'; +import { api } from '../api/client'; +import { useAction, useQuery } from '../lib/hooks'; +import { useToast } from '../lib/toast'; import { ago, initials, num, presence, recent, watchTime, when } from '../lib/format'; -import { - Banner, - Card, - EmptyRow, - Loading, - PageHead, - TableWrap, - Tag, - Tiles, -} from '../components/ui'; -import type { KnownClient } from '../api/types'; - -/* A directory, and only a directory. Everything you can *do* to a person lives on their own - page: this list used to render a full seventeen-control settings editor for every account - at once, which meant the page grew with the household and an operator scrolled past four - other people's preferences to reach the one they came for. */ - -/* Watch time comes from Tracearr, and `matched` is the field that matters: a household - running no Tracearr, and a person Tracearr has never seen, both arrive as zeroes. Drawing - those as "0 min this week" would have an operator asking why somebody stopped watching - when the real answer is that nothing was asked. */ -interface WatchTime { - matched: boolean; - tracearrUsername?: string; - weekMs: number; - monthMs: number; - totalMs: number; - weekSessions: number; - monthSessions: number; - lastWatchedAt?: string; -} +import { Banner, Button, Card, Confirm, EmptyRow, Loading, PageHead, TableWrap, Tag, Tiles, Toggle } from '../components/ui'; +interface Device { id: string; name: string; version: string; lastSeen: string; lastIp?: string; } +interface WatchTime { matched: boolean; weekMs: number; monthMs: number; } interface Account { - id: string; - username: string; - initials: string; - shortName: string; - lastSeen: string; - devices: KnownClient[] | null; - recommendations?: { prompted?: boolean; completed?: boolean }; - watchTime?: WatchTime; + id: string; username: string; initials: string; shortName: string; lastSeen: string; devices: Device[] | null; + enabled: boolean; lastIp?: string; notifications: { enabled: boolean; [key: string]: unknown }; watchTime?: WatchTime; } +interface AccountsResponse { accounts: Account[] | null; } +interface StatusResponse { updatePolicy?: { latestVersion?: string }; } -interface AccountsResponse { - accounts: Account[] | null; -} - -/** seenAt is a timestamp as a number, with anything unreadable sorting last rather than - * first — an invalid date yields NaN, and NaN comparisons would scatter those rows. */ -function seenAt(value: string | undefined): number { - const at = value ? new Date(value).getTime() : 0; - return Number.isFinite(at) ? at : 0; -} - -/** A dash, and it says why on hover. Watch time comes from Tracearr; a household running - * none and a person it has never matched are both "not measured", never "none". */ -function NotMeasured() { - return ( - - — - - ); -} +function seenAt(value: string | undefined): number { const at = value ? new Date(value).getTime() : 0; return Number.isFinite(at) ? at : 0; } +function NotMeasured() { return ; } export function AccountsPage() { - const { data, error, loading } = useQuery('/admin/api/accounts', { - pollMs: 60_000, - }); + const { data, error, loading, reload } = useQuery('/admin/api/accounts', { pollMs: 60_000 }); + const { data: status } = useQuery('/admin/api/status', { pollMs: 60_000 }); + const { busy, run } = useAction(); + const { wrap } = useToast(); + const [expanded, setExpanded] = useState(null); + const [confirm, setConfirm] = useState(null); const accounts = data?.accounts ?? []; const devices = accounts.flatMap((account) => account.devices ?? []); - const completed = accounts.filter((account) => account.recommendations?.completed).length; - const queued = accounts.filter( - (account) => 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 latest = status?.updatePolicy?.latestVersion ?? ''; + const current = (version: string) => Boolean(version && latest && version === latest); + const needsUpdate = devices.filter((device) => device.version && latest && device.version !== latest).length; const tracked = accounts.filter((account) => account.watchTime?.matched); const weekMs = tracked.reduce((total, account) => total + (account.watchTime?.weekMs ?? 0), 0); + const rows = [...accounts].sort((a, b) => seenAt(b.lastSeen) - seenAt(a.lastSeen)); + const path = (id: string, suffix: string) => `/admin/api/accounts/${encodeURIComponent(id)}${suffix}`; - /* Most recently seen first, so the table means something without being sorted. Whoever - is using Memby right now is the row an operator opening this page is looking for, and a - person who has never signed in from a device sorts to the bottom rather than the top. */ - const rows = [...accounts].sort( - (a, b) => seenAt(b.lastSeen) - seenAt(a.lastSeen), - ); + const saveNotifications = async (account: Account, enabled: boolean) => run(`notifications-${account.id}`, () => wrap(() => api.put(path(account.id, '/notifications'), { ...account.notifications, enabled }), `Notifications ${enabled ? 'enabled' : 'disabled'} for ${account.username}`).then(() => reload())); + const saveEnabled = async (account: Account) => { const enabled = !account.enabled; await run(`enabled-${account.id}`, () => wrap(() => api.put(path(account.id, '/enabled'), { enabled }), `${account.username} ${enabled ? 'enabled' : 'disabled'}`).then(() => { setConfirm(null); return reload(); })); }; + const forceUpdate = async (account: Account) => run(`update-${account.id}`, () => wrap(() => api.post(path(account.id, '/force-update')), `Update request queued for ${account.username}`)); - return ( - <> - - - - {loading ? ( - - ) : ( - <> - recent(device.lastSeen)).length), - icon: 'pulse', - tone: 'ok', - }, - { 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: 'watch time this week', - value: watchTime(weekMs), - icon: 'pulse' as const, - tone: 'data' as const, - }, - ] - : []), - ]} - /> - - - - - - - - - - - - - - - - - {rows.length === 0 ? ( - - No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here. - - ) : ( - rows.map((account) => { - const list = account.devices ?? []; - const active = list.filter((device) => recent(device.lastSeen)).length; - const state = account.recommendations?.completed - ? { label: 'personalised', tone: 'ok' as const } - : account.recommendations?.prompted - ? { label: 'prompt queued', tone: 'warn' as const } - : { label: 'not invited', tone: undefined }; - const seen = presence(account.lastSeen); - const watched = account.watchTime; - return ( - - - {/* Blank is the ordinary state and not a gap: the launcher greets - somebody by their account name unless an operator has given - Memby a friendlier one, and saying so beats a bare dash. */} - - - {/* The month sits beside the week because a quiet week only means - something next to the month around it. Both are a dash rather - than a zero for somebody Tracearr has never seen: "0 min" would - have an operator investigating a person when the real answer is - that nothing was ever asked. */} - - - - - - ); - }) - )} - -
UserShort nameDevicesThis weekThis monthRecommendationsLast seen
- - - {account.initials || initials(account.username)} - - {account.username || 'Unnamed user'} - - - - {account.shortName || ( - - account name - - )} - - {num(list.length)} - {/* Only where there is something to say. A sub-line under every - row reading "0 active now" is a column of noise. */} - {active ? {num(active)} active now : null} - - {watched?.matched ? watchTime(watched.weekMs) : } - - {watched?.matched ? watchTime(watched.monthMs) : } - - {state.label} - - {ago(account.lastSeen)} -
-
-
- - )} - - ); + return <> + + + {loading ? : <> + recent(device.lastSeen)).length), icon: 'pulse', tone: 'ok' }, + { label: 'devices up to date', value: num(devices.filter((device) => current(device.version)).length), icon: 'check', tone: 'ok' }, + { label: 'devices requiring update', value: num(needsUpdate), icon: 'alert', tone: needsUpdate ? 'warn' : 'note' }, + ...(tracked.length ? [{ label: 'watch time this week', value: watchTime(weekMs), icon: 'pulse' as const, tone: 'data' as const }] : []), + ]} /> + + + + {rows.length === 0 ? No one has signed in to Memby yet. Emby-only accounts are intentionally not listed here. : rows.map((account) => { + const list = account.devices ?? []; const seen = presence(account.lastSeen); const watched = account.watchTime; const open = expanded === account.id; const first = list[0]; + return <> + + + + + + + + + + + + {open ? : null} + ; + })} +
UserDevicesVersionRemote IPThis weekThis monthLast seenNotificationsEnabledActions
{account.initials || initials(account.username)}{account.username || 'Unnamed user'}{first?.version || Unknown}{first?.version && current(first.version) ? current : first?.version && latest ? update : null}{list.length > 1 ? {list.length - 1} more device{list.length === 2 ? '' : 's'} : null}{first?.lastIp || account.lastIp || }{watched?.matched ? watchTime(watched.weekMs) : }{watched?.matched ? watchTime(watched.monthMs) : }{ago(account.lastSeen)} void saveNotifications(account, next)} /> setConfirm(account)} />
{list.map((device) =>
{device.name || 'Unnamed device'}{device.version || 'Version unknown'}{device.lastIp || 'IP not recorded'}last seen {ago(device.lastSeen)}
)}
+ } + {confirm ? setConfirm(null)} onConfirm={() => void saveEnabled(confirm)} /> : null} + ; } - -/** The account page reads the same list to find the person it is about. */ export type { Account, AccountsResponse }; diff --git a/admin-ui/src/pages/JourneyViewer.tsx b/admin-ui/src/pages/JourneyViewer.tsx index 2d6fe95..777358f 100644 --- a/admin-ui/src/pages/JourneyViewer.tsx +++ b/admin-ui/src/pages/JourneyViewer.tsx @@ -2,9 +2,9 @@ import { useMemo } from 'react'; import { Link, useParams } from 'react-router-dom'; import { query } from '../api/client'; import { useQuery } from '../lib/hooks'; -import { when } from '../lib/format'; +import { num, when } from '../lib/format'; import { Icon } from '../components/Icon'; -import { Banner, Card, Loading, PageHead, Tag } from '../components/ui'; +import { Banner, Card, Loading, PageHead, TableWrap, Tag } from '../components/ui'; interface JourneyEvent { journeyId: string; @@ -25,12 +25,20 @@ interface JourneyEvent { interface JourneyResponse { users: { userId: string; username: string }[] | null; + featureUsage: { feature: string; users: number; uses: number; journeys: number; activeUserRate: number; lastUsedAt: string }[] | null; + featureBreakdown: { feature: string; subFeature: string; action: string; users: number; uses: number; journeys: number; lastUsedAt: string }[] | null; events: JourneyEvent[] | null; } const label = (value: string | undefined | null) => { const text = String(value || '—').replaceAll('_', ' '); - return text === 'favorites' ? 'Favourites' : text; + const labels: Record = { + favorites: 'Favourites', continue_watching: 'Continue Watching', recommendation: 'Recommendations', + for_you: 'Recommendations', screen_view: 'Viewed', open: 'Opened', close: 'Closed', select: 'Selected', + request: 'Requested', start: 'Started', stop: 'Stopped', complete: 'Completed', text: 'Text Search', + voice: 'Voice Search', journey_start: 'Started Session', journey_end: 'Ended Session', + }; + return labels[String(value || '')] ?? text.replace(/\b\w/g, (character) => character.toUpperCase()); }; const place = (event: JourneyEvent | undefined) => label(event?.target || event?.screen || event?.source || event?.feature); @@ -104,10 +112,18 @@ export function JourneyViewerPage() { .sort((left, right) => (right[0]?.occurredAt ?? '').localeCompare(left[0]?.occurredAt ?? '')); }, [data?.events]); const journeys = sessions.flatMap((session) => splitViewingJourneys(session).map((events, index) => ({ events, key: `${session[0]?.journeyId}:${index}` }))); + const featureUsage = data?.featureUsage ?? []; + const featureBreakdown = data?.featureBreakdown ?? []; return <> Journeys} /> + {!loading && + + {featureUsage.length === 0 ? : featureUsage.map((feature) => )} +
FeatureUsesVisitsLast used
No feature usage recorded for this viewer.
{label(feature.feature)}{num(feature.users)} viewer{num(feature.uses)}{num(feature.journeys)}{when(feature.lastUsedAt)}
+ {featureBreakdown.length > 0 &&

Breakdown: {featureBreakdown.slice(0, 8).map((entry) => `${label(entry.feature)} · ${label(entry.subFeature)} · ${label(entry.action)} (${num(entry.uses)})`).join(' · ')}

} +
} {loading ? :
{journeys.length === 0 ?

No journeys recorded for this viewer.

: journeys.map((journey, index) => { diff --git a/admin-ui/src/pages/Journeys.tsx b/admin-ui/src/pages/Journeys.tsx index abee12d..cb4095d 100644 --- a/admin-ui/src/pages/Journeys.tsx +++ b/admin-ui/src/pages/Journeys.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { query } from '../api/client'; import { useQuery } from '../lib/hooks'; -import { duration, num, percent, when } from '../lib/format'; +import { duration, num, percent } from '../lib/format'; import { Banner, Card, @@ -13,30 +13,24 @@ import { Meter, PageHead, TableWrap, - Tag, Tiles, } from '../components/ui'; -/* Memby's major feature catalogue, in the order it is presented. - * - * It is a list here rather than derived from what has been used, and that is the whole - * point of the table it feeds: a feature nobody has touched has no row in the analytics - * and would simply be absent, which is indistinguishable from a feature that does not - * exist. Listing it and showing a zero is what makes "not used" an answer. */ -const FEATURE_CATALOGUE = [ - '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', -]; - /** The wire values are ids; this is the render, so it is spelled — the same boundary the * launcher's own row titles observe. */ function label(value: string | undefined | null): string { const text = String(value || '—').replaceAll('_', ' '); - if (text === 'favorites') return 'Favourites'; - if (text === 'abandoned') return 'Abandoned / interrupted'; - return text; + const labels: Record = { + favorites: 'Favourites', continue: 'Continue Watching', continue_watching: 'Continue Watching', + recent_searches: 'Recent Searches', genre_browse: 'Genre Browse', for_you: 'Recommendations', + recommendation: 'Recommendations', for_you_time: 'Recommendation Timing', my_shows: 'My Shows', + magic_movie: 'Magic Movie', detail_page: 'Details', home_hero: 'Home Hero', + screen_view: 'Viewed', open: 'Opened', close: 'Closed', select: 'Selected', request: 'Requested', + start: 'Started', stop: 'Stopped', complete: 'Completed', change: 'Changed', toggle: 'Toggled', + submit: 'Submitted', journey_start: 'Started Session', journey_end: 'Ended Session', + abandoned: 'Abandoned / interrupted', text: 'Text Search', voice: 'Voice Search', + }; + return labels[String(value || '')] ?? text.replace(/\b\w/g, (character) => character.toUpperCase()); } interface JourneysResponse { @@ -55,6 +49,8 @@ interface JourneysResponse { }; users: { userId: string; username: string }[] | null; features: { feature: string; uses: number; lastUsedAt: string }[] | null; + featureUsage: { feature: string; users: number; uses: number; journeys: number; activeUserRate: number; lastUsedAt: string }[] | null; + featureBreakdown: { feature: string; subFeature: string; action: string; users: number; uses: number; journeys: number; lastUsedAt: string }[] | null; actions: { category: string; action: string; events: number; journeys: number }[] | null; paths: { from: string; to: string; count: number }[] | null; } @@ -68,25 +64,11 @@ export function JourneysPage() { const stats = data?.stats; const users = data?.users ?? []; - const actions = data?.actions ?? []; + const featureUsage = data?.featureUsage ?? []; + const featureBreakdown = data?.featureBreakdown ?? []; const paths = data?.paths ?? []; const topPath = paths[0]; - const features = useMemo(() => { - const used = new Map((data?.features ?? []).map((feature) => [feature.feature, feature])); - const position = new Map(FEATURE_CATALOGUE.map((name, index) => [name, index])); - return [...new Set([...FEATURE_CATALOGUE, ...used.keys()])] - .map((name) => ({ name, stat: used.get(name) })) - .sort((left, right) => { - const byUse = (right.stat?.uses ?? 0) - (left.stat?.uses ?? 0); - if (byUse) return byUse; - return ( - (position.get(left.name) ?? Number.MAX_SAFE_INTEGER) - - (position.get(right.name) ?? Number.MAX_SAFE_INTEGER) - ); - }); - }, [data?.features]); - // Individual visits are shown only for one person: across a household they are a wall // of cards with nothing to compare against, and the question they answer is always // "what did *they* do". @@ -182,8 +164,8 @@ export function JourneysPage() { @@ -191,23 +173,24 @@ export function JourneysPage() { - + + + - {actions.length === 0 ? ( - No significant actions in this window. + {featureUsage.length === 0 ? ( + No feature usage in this window. ) : ( - actions.map((action) => ( - - - - + featureUsage.map((feature) => ( + + + + + + )) )} @@ -217,8 +200,8 @@ export function JourneysPage() { @@ -226,20 +209,24 @@ export function JourneysPage() {
ActionFeatureUsers Uses VisitsActive users
- {label(action.action)} - {label(action.category)} - {num(action.events)}{num(action.journeys)}
{label(feature.feature)}{num(feature.users)}{num(feature.uses)}{num(feature.journeys)}{percent(feature.activeUserRate)}
- + + + + - {paths.length === 0 ? ( - No repeated paths in this window. + {featureBreakdown.length === 0 ? ( + No feature details in this window. ) : ( - paths.map((entry, index) => ( - - - + featureBreakdown.slice(0, 30).map((entry, index) => ( + + + + + + )) )} @@ -250,8 +237,8 @@ export function JourneysPage() { @@ -259,32 +246,14 @@ export function JourneysPage() {
RouteFeature / sub-featureActionUsers TimesVisits
- {label(entry.from)} {label(entry.to)} - {num(entry.count)}
{label(entry.feature)}{label(entry.subFeature)}{label(entry.action)}{num(entry.users)}{num(entry.uses)}{num(entry.journeys)}
- - - - + + - {features.map(({ name, stat }) => { - const uses = stat?.uses ?? 0; - return ( - - - - - - - ); - })} + {paths.length === 0 ? No repeated paths in this window. : paths.map((entry, index) => ( + + ))}
FeatureUsesLast usedStatusRouteTimes
{label(name)}{num(uses)}{stat ? when(stat.lastUsedAt) : '—'} - {uses === 0 ? ( - not used - ) : uses < 3 ? ( - rare - ) : ( - used - )} -
{label(entry.from)} {label(entry.to)}{num(entry.count)}
diff --git a/admin-ui/src/styles.css b/admin-ui/src/styles.css index a5adca2..8dd6441 100644 --- a/admin-ui/src/styles.css +++ b/admin-ui/src/styles.css @@ -882,6 +882,59 @@ a { font: inherit; } +/* A gateway restart is a console-wide state, not a page error. Keep the page mounted so + filters and navigation survive the short outage, while making it impossible for a proxy + response or a collection of stale banners to become the operator's view. */ +.reconnect-overlay { + position: fixed; + inset: 0; + z-index: 100; + display: grid; + place-items: center; + padding: 24px; + background: rgba(3, 6, 10, .86); + backdrop-filter: blur(5px); +} +.reconnect-panel { + display: grid; + justify-items: center; + width: min(440px, 100%); + padding: 34px 32px 30px; + border: 1px solid var(--line); + border-radius: 14px; + background: linear-gradient(145deg, var(--surface-lift), var(--surface)); + box-shadow: 0 24px 80px rgba(0, 0, 0, .4); + text-align: center; +} +.reconnect-spinner { + width: 30px; + height: 30px; + margin-bottom: 18px; + border: 3px solid var(--line); + border-top-color: var(--accent-ink); + border-radius: 50%; + animation: reconnect-spin .9s linear infinite; +} +.reconnect-panel h1 { + margin: 0; + font-size: 21px; + letter-spacing: -.02em; +} +.reconnect-panel p { + max-width: 35ch; + margin: 10px 0 18px; + color: var(--muted); + font-size: 13px; + line-height: 1.55; +} +.reconnect-panel small { + color: var(--quiet); + font: 11.5px/1.4 var(--mono); +} +@keyframes reconnect-spin { + to { transform: rotate(360deg); } +} + /* ---------- cards ---------- */ .card { @@ -1384,6 +1437,22 @@ td.mono { font: 12px/1.5 var(--mono); color: var(--muted); } +.link-button { + border: 0; + padding: 0; + background: none; + color: var(--accent-ink); + font: inherit; + text-decoration: underline; + text-decoration-color: var(--line); + text-underline-offset: 3px; + cursor: pointer; +} +.is-disabled td { opacity: .62; } +.account-device-detail > td { padding: 0 0 12px; background: var(--surface-sunken, transparent); } +.device-breakdown { display: grid; gap: 6px; padding: 10px 12px 10px 46px; border-top: 1px solid var(--line-soft); } +.device-breakdown-row { display: grid; grid-template-columns: minmax(150px, 1.4fr) minmax(90px, .7fr) minmax(130px, 1fr) minmax(100px, 1fr); gap: 12px; align-items: center; padding: 8px 10px; border: 1px solid var(--line-soft); border-radius: var(--radius-sm); background: var(--surface-lift); font-size: 12px; } +@media (max-width: 1100px) { .device-breakdown-row { grid-template-columns: 1fr 1fr; } } td.nowrap, th.nowrap { white-space: nowrap; diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b579481..2339c46 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,7 +38,7 @@ val membyGatewayUrl: String = (project.findProperty("memby.gatewayUrl") as Strin val membyDiagnosticLogLevel: String = (project.findProperty("memby.diagnosticLogLevel") as String?) ?.trim()?.uppercase()?.takeIf { it in setOf("INFO", "DEBUG", "TRACE") } ?: "INFO" -val defaultVersionName = "0.3.20" +val defaultVersionName = "0.3.21" val membyVersionName: String = (project.findProperty("memby.versionName") as String?) ?.trim() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0bb9a38..d474b00 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -68,9 +68,9 @@ android:name=".MembyApp" android:allowBackup="true" android:banner="@drawable/app_banner" - android:icon="@drawable/emby_logo" + android:icon="@drawable/memby_mark" android:label="@string/app_name" - android:roundIcon="@drawable/emby_logo" + android:roundIcon="@drawable/memby_mark" android:supportsRtl="true" android:usesCleartextTraffic="true" android:theme="@style/Theme.Memby"> @@ -145,7 +145,7 @@ diff --git a/app/src/main/java/com/ponzischeme89/memby/data/DetailExperience.kt b/app/src/main/java/com/ponzischeme89/memby/data/DetailExperience.kt new file mode 100644 index 0000000..1c95ca2 --- /dev/null +++ b/app/src/main/java/com/ponzischeme89/memby/data/DetailExperience.kt @@ -0,0 +1,12 @@ +package com.ponzischeme89.memby.data + +/** + * The one legal non-default value. Anything else — missing, blank, a value a future variant + * introduces that this build does not know about — must fall back to "v1" rather than be + * handed to the UI to guess about. + */ +private const val DETAIL_EXPERIENCE_V2 = "v2" +const val DETAIL_EXPERIENCE_DEFAULT = "v1" + +fun detailExperienceOrDefault(raw: String): String = + raw.takeIf { it == DETAIL_EXPERIENCE_V2 } ?: DETAIL_EXPERIENCE_DEFAULT diff --git a/app/src/main/java/com/ponzischeme89/memby/data/EmbyRepository.kt b/app/src/main/java/com/ponzischeme89/memby/data/EmbyRepository.kt index 66246cd..3a55530 100644 --- a/app/src/main/java/com/ponzischeme89/memby/data/EmbyRepository.kt +++ b/app/src/main/java/com/ponzischeme89/memby/data/EmbyRepository.kt @@ -294,7 +294,10 @@ class EmbyRepository internal constructor( * layout and must not collect a flow to answer a question that changes once a year. */ val showTitleLogo: Boolean get() = snapshot.showTitleLogo - private val _playbackStops = MutableSharedFlow(extraBufferCapacity = 1) + // Carries whether the stopped item finished, computed at the report itself rather than + // read back from the [playbackPositions] side channel afterwards — the two are separate + // flows collected by separate coroutines, and nothing orders one against the other. + private val _playbackStops = MutableSharedFlow(extraBufferCapacity = 1) val playbackStops = _playbackStops.asSharedFlow() /** @@ -2715,7 +2718,9 @@ class EmbyRepository internal constructor( clearPlayableCache() // Episode progress and watched badges may have changed during playback. clearSeriesEpisodeCache() - _playbackStops.tryEmit(session.itemId) + _playbackStops.tryEmit( + PlaybackPosition(session.itemId, positionMs.coerceAtLeast(0L), durationMs.coerceAtLeast(0L)), + ) } } diff --git a/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt b/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt index 8e9f921..7ed7953 100644 --- a/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt +++ b/app/src/main/java/com/ponzischeme89/memby/data/MaintenanceMonitor.kt @@ -105,6 +105,7 @@ class MaintenanceMonitor( private val _requestsAllowed = MutableStateFlow(false) private val _gatewayVersion = MutableStateFlow("") private val _embyVersion = MutableStateFlow("") + private val _detailExperience = MutableStateFlow(DETAIL_EXPERIENCE_DEFAULT) /** * Which colour scheme this viewer's televisions should be painted, as an id and a @@ -129,6 +130,14 @@ class MaintenanceMonitor( */ val heroRevision: StateFlow = _heroRevision.asStateFlow() + /** + * The v2 detail-page experiment for this viewer/device, already validated by + * [detailExperienceOrDefault] against garbage or a future value this build does not + * understand. "v1" whenever the server has not said otherwise — signed out, mid-outage, + * or on a gateway that predates the field — which is the existing detail page, unchanged. + */ + val detailExperience: StateFlow = _detailExperience.asStateFlow() + /** * The viewer's server-held settings revision, as of the last successful poll. This is * how an operator's push reaches a television: the number changes, [PreferencesSync] @@ -300,6 +309,7 @@ class MaintenanceMonitor( _metadataHeroTimeRemainingColour.value = METADATA_HERO_TIME_COLOUR_GREEN _theme.value = GatewayThemeStatus() _heroRevision.value = "" + _detailExperience.value = DETAIL_EXPERIENCE_DEFAULT _installPermissionPrompt.value = false _genreBrowserEnabled.value = false _tvCalendarEnabled.value = false @@ -348,6 +358,7 @@ class MaintenanceMonitor( ?: METADATA_HERO_TIME_COLOUR_GREEN _theme.value = status.theme _heroRevision.value = status.hero.revision + _detailExperience.value = detailExperienceOrDefault(status.detailExperience) _installPermissionPrompt.value = status.features[INSTALL_PERMISSION_FEATURE] == true _genreBrowserEnabled.value = status.features[GENRE_BROWSER_FEATURE] == true @@ -396,6 +407,7 @@ class MaintenanceMonitor( _metadataHeroTimeRemainingColour.value = METADATA_HERO_TIME_COLOUR_GREEN _theme.value = GatewayThemeStatus() _heroRevision.value = "" + _detailExperience.value = DETAIL_EXPERIENCE_DEFAULT _installPermissionPrompt.value = false _genreBrowserEnabled.value = false _tvCalendarEnabled.value = false diff --git a/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt b/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt index bc0dd30..9d6f8ef 100644 --- a/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt +++ b/app/src/main/java/com/ponzischeme89/memby/data/model/GatewayModels.kt @@ -275,6 +275,13 @@ data class GatewayServiceStatus( * exactly how the launcher behaved before this existed. */ val hero: GatewayHeroStatus = GatewayHeroStatus(), + /** + * The v2 detail-page experiment. A plain string rather than a boolean feature flag, + * scoped per user/device like [theme]. Missing (a gateway that predates it) or anything + * other than exactly "v2" decodes to the compiled default of "v1" — see + * [com.ponzischeme89.memby.data.detailExperienceOrDefault]. + */ + val detailExperience: String = "v1", ) @Serializable diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/HomeOverlays.kt b/app/src/main/java/com/ponzischeme89/memby/ui/HomeOverlays.kt index dd164fe..92e2732 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/HomeOverlays.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/HomeOverlays.kt @@ -322,6 +322,14 @@ internal fun FocusedDetailsOverlay( homeViewModel: HomeViewModel, selected: BaseItem, seriesStatusRevision: Long = 0, + /** + * The server-controlled detail-page experiment, already validated by + * [com.ponzischeme89.memby.data.detailExperienceOrDefault] to "v1" or "v2". Only Home's + * movie/series posters offer the seamless "v2" reveal — an episode (reached from + * Continue Watching, never from a poster) and a Radarr-only card (no shelf to reveal + * from) always take the ordinary v1 overlay. + */ + detailExperience: String = com.ponzischeme89.memby.data.DETAIL_EXPERIENCE_DEFAULT, restorePosition: Boolean, onPlay: (BaseItem) -> Unit, onPlayTrailer: (BaseItem) -> Unit, @@ -360,6 +368,39 @@ internal fun FocusedDetailsOverlay( } } val item = detailItemForRoute(selected, focusedItem, detailMetadata) + // The seamless "v2" reveal only makes sense where there was a shelf to reveal from: a + // Radarr-only card has no Emby item behind it (its own page, see [RadarrMovieDetailsOverlay]) + // and an episode is reached from Continue Watching rather than a poster press, so both + // keep the ordinary v1 overlay regardless of the operator's setting. + val isSeamless = com.ponzischeme89.memby.data.detailExperienceOrDefault(detailExperience) == "v2" && + !item.isRadarrOnly && !item.isEpisode + var playPressed by remember(item.id) { mutableStateOf(false) } + LaunchedEffect(item.id, isSeamless) { + homeViewModel.trackJourney( + category = "details", action = "opened", screen = "details", + feature = if (isSeamless) "detail_v2" else "detail_v1", + itemName = item.name, itemType = item.type, + ) + } + val trackedOnPlay: (BaseItem) -> Unit = { playedItem -> + playPressed = true + homeViewModel.trackJourney( + category = "details", action = "play_pressed", screen = "details", + feature = if (isSeamless) "detail_v2" else "detail_v1", + itemName = playedItem.name, itemType = playedItem.type, + ) + onPlay(playedItem) + } + val trackedOnClose: () -> Unit = { + if (!playPressed) { + homeViewModel.trackJourney( + category = "details", action = "closed_without_playback", screen = "details", + feature = if (isSeamless) "detail_v2" else "detail_v1", + itemName = item.name, itemType = item.type, + ) + } + onClose() + } if (item.isRadarrOnly) { // A film Radarr is tracking that Emby has never imported. It is the one card with a // page of its own rather than an Emby one — see [RadarrMovieDetailsOverlay] for why @@ -367,58 +408,80 @@ internal fun FocusedDetailsOverlay( RadarrMovieDetailsOverlay( card = item, onPlayTrailer = onPlayTrailer, - onClose = onClose, + onClose = trackedOnClose, onOpenEmbyItem = onOpenEmbyItem, ) } else if (item.isSeries) { - SeriesDetailsOverlay( - item = item, - onPlay = onPlay, - onPlayTrailer = onPlayTrailer, - onToggleFavorite = onToggleFavorite, - isMyShow = isMyShow, - onToggleMyShow = onToggleMyShow, - onSeriesPlayedChanged = { series, played -> - homeViewModel.trackJourney( - category = "library", action = if (played) "mark_played" else "mark_unplayed", - screen = "details", feature = "played_status", itemName = series.name, - itemType = series.type, outcome = "success", - ) - homeViewModel.applySeriesPlayed(series, played) - }, - onSeriesPlayedSettled = homeViewModel::refreshContinueWatching, - onClose = onClose, - onOpenItem = onOpenItem, - restorePosition = restorePosition, - airingNotice = airingNotice, - ) + // Seamless "v2" entry is a fast fade-in over what was already warmed on focus — + // the item, its logo, backdrop and ratings are already resident, and Coil already + // holds the decoded bitmaps, so this costs no re-fetch. v1 gets no wrapper at all, + // so its existing screenshot tests and behaviour are untouched. + AnimatedVisibility( + visible = true, + enter = if (isSeamless) fadeIn(tween(SEAMLESS_REVEAL_DURATION_MS)) else fadeIn(tween(0)), + ) { + SeriesDetailsOverlay( + item = item, + onPlay = trackedOnPlay, + onPlayTrailer = onPlayTrailer, + onToggleFavorite = onToggleFavorite, + isMyShow = isMyShow, + onToggleMyShow = onToggleMyShow, + onSeriesPlayedChanged = { series, played -> + homeViewModel.trackJourney( + category = "library", action = if (played) "mark_played" else "mark_unplayed", + screen = "details", feature = "played_status", itemName = series.name, + itemType = series.type, outcome = "success", + ) + homeViewModel.applySeriesPlayed(series, played) + }, + onSeriesPlayedSettled = homeViewModel::refreshContinueWatching, + onClose = trackedOnClose, + onOpenItem = onOpenItem, + restorePosition = restorePosition, + airingNotice = airingNotice, + ) + } } else if (item.isEpisode) { // An episode arrives here from Continue Watching, where it *is* the thing the // viewer chose. It used to open the movie page, which named the // episode with no way to tell which one it was or where in the show it sat. + // Never eligible for the seamless reveal — see [isSeamless]. EpisodeDetailsOverlay( item = item, - onPlay = onPlay, + onPlay = trackedOnPlay, onToggleFavorite = onToggleFavorite, onTogglePlayed = onTogglePlayed, - onClose = onClose, + onClose = trackedOnClose, onOpenItem = onOpenItem, restorePosition = restorePosition, ) } else { - MediaDetailsOverlay( - item = item, - onPlay = onPlay, - onPlayTrailer = onPlayTrailer, - onToggleFavorite = onToggleFavorite, - onTogglePlayed = onTogglePlayed, - onClose = onClose, - onOpenItem = onOpenItem, - restorePosition = restorePosition, - ) + AnimatedVisibility( + visible = true, + enter = if (isSeamless) fadeIn(tween(SEAMLESS_REVEAL_DURATION_MS)) else fadeIn(tween(0)), + ) { + MediaDetailsOverlay( + item = item, + onPlay = trackedOnPlay, + onPlayTrailer = onPlayTrailer, + onToggleFavorite = onToggleFavorite, + onTogglePlayed = onTogglePlayed, + onClose = trackedOnClose, + onOpenItem = onOpenItem, + restorePosition = restorePosition, + ) + } } } +/** + * How long the v2 seamless reveal fades in the detail hero over the shelf it replaced. Kept + * short and restrained, per the experiment's own "things not moving" premise — this is not + * a cinematic transition, just enough to soften an otherwise instant cut. + */ +private const val SEAMLESS_REVEAL_DURATION_MS = 120 + /** * Combines the three owners involved in a detail page without confusing their lifetimes: * the selected route fixes identity, current focus contributes live user state only when diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt b/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt index f6a44c6..ce39256 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/HomeScreen.kt @@ -266,6 +266,9 @@ internal fun HomeScreen( ServiceLocator.maintenance.metadataHeroTimeRemainingColour.collectAsStateWithLifecycle() val compatibilityNotice by ServiceLocator.maintenance.compatibility.collectAsStateWithLifecycle() val genreBrowserEnabled by ServiceLocator.maintenance.genreBrowserEnabled.collectAsStateWithLifecycle() + // Server-controlled experiment: which detail-page layout Home opens. Already validated + // to "v1" or "v2" by MaintenanceMonitor, so this is safe to branch on directly. + val detailExperience by ServiceLocator.maintenance.detailExperience.collectAsStateWithLifecycle() val tvCalendarEnabled by ServiceLocator.maintenance.tvCalendarEnabled.collectAsStateWithLifecycle() val continueWatchingEnabled by ServiceLocator.maintenance.continueWatchingEnabled.collectAsStateWithLifecycle() @@ -2201,6 +2204,7 @@ internal fun HomeScreen( homeViewModel = homeViewModel, selected = selected, seriesStatusRevision = seriesStatusRevision, + detailExperience = detailExperience, restorePosition = restoreDetailPosition, airingNotice = detailsAiringNotice, onOpenItem = { related -> diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt b/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt index acf372c..3343326 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/HomeViewModel.kt @@ -220,7 +220,16 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() { repository.playbackPositions.collect(::applyPlaybackPosition) } viewModelScope.launch { - repository.playbackStops.collect { refreshWatching(authoritative = false) } + repository.playbackStops.collect { stop -> + if (stop.completed) { + // The optimistic prune in applyPlaybackPosition has already removed the + // finished episode; only the gateway's own answer can put the next one in + // its place, and the stop report just invalidated its cached rows. + refreshAfterCompletion(stop.itemId) + } else { + refreshWatching(authoritative = false) + } + } } viewModelScope.launch { // A D-pad produces focus changes far faster than anything should produce @@ -862,6 +871,29 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() { } } + /** + * Follows a completed episode's stop report with the gateway's own answer, which is what + * actually puts the next episode in the finished one's place — the optimistic prune in + * [applyPlaybackPosition] only ever removes a card, it never adds one. The stop report + * that precedes this is what invalidates the gateway's cached rows, but Emby's own + * bookkeeping can lag behind it by a beat, so an authoritative refresh that comes back + * with the same Continue Watching membership it already had is retried a small, bounded + * number of times rather than trusted on the first try or polled without end. + */ + private suspend fun refreshAfterCompletion(itemId: String) { + val before = continueWatchingIds() + repeat(COMPLETION_REFRESH_ATTEMPTS) { attempt -> + refreshWatching(authoritative = true) + val after = continueWatchingIds() + val settled = (itemId !in after && after != before) || attempt == COMPLETION_REFRESH_ATTEMPTS - 1 + if (settled) return + delay(COMPLETION_REFRESH_RETRY_DELAY_MS) + } + } + + private fun continueWatchingIds(): Set = + _state.value.continueWatching.mapTo(mutableSetOf(), BaseItem::id) + private suspend fun loadContinueWatching(clearLoading: Boolean = true) = load(HomeSection.CONTINUE, clearLoading, { repository.getContinueWatching() }) { state, items -> state.copy( @@ -924,6 +956,11 @@ class HomeViewModel(private val repository: EmbyRepository) : ViewModel() { private const val FOCUS_METADATA_DEBOUNCE_MS = 200L private const val HERO_UPDATE_DEBOUNCE_MS = 120L + /** Bounded retries for [refreshAfterCompletion] — a race with Emby's own bookkeeping, + * not a poll, so this stays small and stops on its own. */ + private const val COMPLETION_REFRESH_ATTEMPTS = 3 + private const val COMPLETION_REFRESH_RETRY_DELAY_MS = 600L + /** * How long focus must rest on a card before lightweight detail work is warmed, measured * from the press that focused it. Deliberately well past diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/player/CountdownRingView.kt b/app/src/main/java/com/ponzischeme89/memby/ui/player/CountdownRingView.kt new file mode 100644 index 0000000..e9ecbe0 --- /dev/null +++ b/app/src/main/java/com/ponzischeme89/memby/ui/player/CountdownRingView.kt @@ -0,0 +1,143 @@ +package com.ponzischeme89.memby.ui.player + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.RectF +import android.graphics.Typeface +import android.util.AttributeSet +import android.view.View +import com.ponzischeme89.memby.ui.theme.membyTypeface +import kotlin.math.ceil +import kotlin.math.min + +/** + * A draining arc with the figure left inside it. + * + * Two things in the player count something down against the playhead — the skip-intro + * offer and the next-up overlay — and both want the same picture. This is that picture and + * nothing else: it runs no animator, holds no timer and knows nothing about what it is + * measuring. The caller advances it from media time, which is what keeps it honest — a + * pause holds the ring where it is and a seek moves it, neither of which a ring counting + * wall-clock seconds could do. + * + * Colours are set rather than derived, because the two callers want different answers: the + * skip-intro ring lives inside a state-list pill and has to invert with it, while the + * next-up ring sits on somebody's programme and is the same white and green wherever the + * picture behind it happens to be light. + */ +open class CountdownRingView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : View(context, attrs, defStyleAttr) { + + private val density = resources.displayMetrics.density + private val ringBounds = RectF() + private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeCap = Paint.Cap.ROUND + strokeWidth = 2.5f * density + } + private val progressPaint = Paint(trackPaint) + private val figurePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + textAlign = Paint.Align.CENTER + typeface = context.membyTypeface(Typeface.BOLD) + color = Color.WHITE + } + + private var figure = "" + private var progress = 1f + + init { + setInk(track = Color.argb(72, 255, 255, 255), arc = Color.WHITE, figure = Color.WHITE) + } + + /** How thick the arc is drawn. The default suits a ring of about 30–40dp. */ + fun setRingWidthDp(widthDp: Float) { + trackPaint.strokeWidth = widthDp * density + progressPaint.strokeWidth = trackPaint.strokeWidth + invalidate() + } + + /** The three colours the ring is made of. Invalidates, so it is safe mid-countdown. */ + fun setInk(track: Int, arc: Int, figure: Int) { + trackPaint.color = track + progressPaint.color = arc + figurePaint.color = figure + invalidate() + } + + /** + * How much is left, and how long the whole thing runs for. + * + * Redraws only when the drawn result would actually differ. This is advanced several + * times a second for a minute or two at a stretch, and a long ring moves by a fraction + * of a degree per tick — invalidating on every one of them would be a couple of hundred + * pointless draws per episode on a box that has a decoder to feed. + */ + fun setRemaining(remainingMs: Long, totalMs: Long) { + val remaining = remainingMs.coerceAtLeast(0L) + val nextFigure = formatRemaining(remaining) + val nextProgress = if (totalMs > 0L) { + (remaining.toFloat() / totalMs.toFloat()).coerceIn(0f, 1f) + } else { + 0f + } + // A degree is about the smallest movement worth a redraw; below that the arc lands + // on the same pixels. + val moved = kotlin.math.abs(nextProgress - progress) * 360f >= 1f + if (nextFigure == figure && !moved) return + figure = nextFigure + progress = nextProgress + describe(nextFigure)?.let { contentDescription = it } + invalidate() + } + + /** What a screen reader should make of the figure. Nothing, unless a subclass says. */ + protected open fun describe(figure: String): CharSequence? = null + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val strokeInset = trackPaint.strokeWidth / 2f + val diameter = min(width, height).toFloat() + val left = (width - diameter) / 2f + strokeInset + val top = (height - diameter) / 2f + strokeInset + ringBounds.set( + left, + top, + left + diameter - trackPaint.strokeWidth, + top + diameter - trackPaint.strokeWidth, + ) + canvas.drawOval(ringBounds, trackPaint) + if (progress > 0f) { + // Anticlockwise from the top, so the ring empties the way a clock hand would + // sweep back rather than filling up as the thing it measures runs out. + canvas.drawArc(ringBounds, -90f, -360f * progress, false, progressPaint) + } + if (figure.isEmpty()) return + figurePaint.textSize = figureTextSize(diameter, figure.length) + val baseline = height / 2f - (figurePaint.ascent() + figurePaint.descent()) / 2f + canvas.drawText(figure, width / 2f, baseline, figurePaint) + } + + /** + * The figure has to fit inside the ring, and "1:58" is twice the width of "12". Sizing + * from the string's own length is what stops a two-minute opening printing over its own + * arc — an intro is commonly long enough to be counted in minutes, so this is the + * ordinary case rather than the edge one. + */ + private fun figureTextSize(diameter: Float, characters: Int): Float = + diameter * if (characters >= 4) 0.30f else 0.42f +} + +/** + * "1:58" over a minute, "58" under it. Never "0:58": a bare figure is read at a glance in a + * ring this size, and the colon is only worth its width once there are minutes to separate. + */ +internal fun formatRemaining(remainingMs: Long): String { + val seconds = ceil(remainingMs.coerceAtLeast(0L) / 1_000.0).toInt() + if (seconds < 60) return seconds.toString() + return "${seconds / 60}:${(seconds % 60).toString().padStart(2, '0')}" +} diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt b/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt index e7585e5..cba7f10 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/player/PlayerActivity.kt @@ -399,6 +399,17 @@ class PlayerActivity : ComponentActivity() { private var currentMediaSubtitles: List = emptyList() private var nextUpBanner: View? = null private var nextUpCountdown: TextView? = null + private var nextUpRing: CountdownRingView? = null + private var nextUpLogo: ImageView? = null + private var nextUpSeries: TextView? = null + /** + * The logo the bar is currently wearing, so the identity is bound once per episode + * rather than on every 250ms tick of the countdown. Coil would answer the repeats from + * its memory cache, but each one is still a request built, a listener attached and a + * drawable re-tinted while a decoder is being fed. + */ + private var nextUpBoundLogoUrl: String? = null + private var nextUpBoundItemId: String? = null private var nextUpDismissed = false private var advancing = false @@ -2710,7 +2721,6 @@ class PlayerActivity : ComponentActivity() { playerView?.isControllerFullyVisible != true && !castPanelVisible.value && subtitleOverlay?.isVisible != true && - nextUpBanner?.isVisible != true && creditsView?.isVisible != true && loadingView?.isVisible != true && errorView?.isVisible != true @@ -3046,7 +3056,6 @@ class PlayerActivity : ComponentActivity() { playerView?.isControllerFullyVisible != true && !castPanelVisible.value && subtitleOverlay?.isVisible != true && - nextUpBanner?.isVisible != true && creditsView?.isVisible != true && loadingView?.isVisible != true && errorView?.isVisible != true @@ -3202,11 +3211,18 @@ class PlayerActivity : ComponentActivity() { val banner = findViewById(R.id.player_next_up) nextUpBanner = banner nextUpCountdown = banner.findViewById(R.id.player_next_up_countdown) - banner.findViewById(R.id.player_next_up_play).setOnClickListener { - startNextEpisode() - } - banner.findViewById(R.id.player_next_up_dismiss).setOnClickListener { - dismissNextUp() + nextUpLogo = banner.findViewById(R.id.player_next_up_logo) + nextUpSeries = banner.findViewById(R.id.player_next_up_series) + nextUpRing = banner.findViewById(R.id.player_next_up_ring)?.apply { + // Fixed rather than taken from a drawable state, because nothing here is + // focusable: the ring sits on somebody's programme and has to read against + // whatever the picture behind it happens to be doing. + setRingWidthDp(2.5f) + setInk( + track = Color.argb(56, 255, 255, 255), + arc = NEXT_UP_ACCENT, + figure = Color.WHITE, + ) } } @@ -3232,6 +3248,10 @@ class PlayerActivity : ComponentActivity() { // Gating the lookup on it is what left the credits pane, the next-up banner and // the ended frame reading a field that was permanently null with the setting off. val resolved = nextUpResolver.resolve(id) + // Binds the bar's logo a minute or more before it is drawn, and warms Coil with + // it in the same movement. Safe to do for an episode nobody reaches the end of: + // the bar is hidden, and the next `begin` clears what was bound. + resolved?.let(::bindNextUpIdentity) resolved?.imageUrl?.let { imageUrl -> imageLoader.enqueue( ImageRequest.Builder(this@PlayerActivity) @@ -3563,16 +3583,25 @@ class PlayerActivity : ComponentActivity() { } nextUpDismissed -> Unit remainingMs == 0L -> if (autoAdvance) startNextEpisode() + // Something that owns the screen is up, or the transport is occupying the same + // bottom edge. The bar has nothing to say that is worth being drawn under + // either, and it comes straight back when they go. + !nextUpCanShow() -> hideNextUp() else -> { showNextUp(next) - nextUpCountdown?.apply { - if (autoAdvance) { - val seconds = ceil(remainingMs / 1_000.0).toInt() - text = getString(R.string.next_up_starting_in, seconds) - visibility = View.VISIBLE - } else { - visibility = View.GONE - } + // The countdown and its ring are a promise that something will happen by + // itself, so they are drawn only where that is true. With automatic advance + // off the bar still says what is next and the transport's Next Episode + // button still starts it — the offer is not the transition. + nextUpCountdown?.visibility = if (autoAdvance) View.VISIBLE else View.GONE + nextUpRing?.visibility = if (autoAdvance) View.VISIBLE else View.GONE + if (autoAdvance) { + nextUpCountdown?.text = + getString(R.string.next_up_starting_in, formatRemaining(remainingMs)) + // Advanced from the playhead, not from a timer: pausing inside the last + // minute holds the ring where it is and seeking back out of the window + // takes the bar away entirely. + nextUpRing?.setRemaining(remainingMs, NEXT_UP_LEAD_MS) } } } @@ -3714,37 +3743,76 @@ class PlayerActivity : ComponentActivity() { } } - @OptIn(UnstableApi::class) + /** + * Whether the bar has anything to say and anywhere to say it. + * + * Every other overlay in this player *owns* the screen while it is up — the drop-up, + * the cast panel, the credits pane, the error and loading surfaces — and this one owns + * nothing at all, so wherever one of those is up this simply stands down rather than + * being drawn underneath it. The transport is in the list for the plainest reason: + * it is a full-width strip along the same bottom edge, and the two would overlap. + */ + private fun nextUpCanShow(): Boolean = + !prerollActive && + playerView?.isControllerFullyVisible != true && + !castPanelVisible.value && + subtitleOverlay?.isVisible != true && + skipIntroView?.isVisible != true && + creditsView?.isVisible != true && + loadingView?.isVisible != true && + errorView?.isVisible != true + + /** + * Puts the show's own title treatment on the bar, with its name as the fallback. + * + * Called when the next episode is *resolved* rather than when the bar appears, which + * is a minute or more of lead time on the ordinary path — so by the time the bar fades + * in the logo is already in place and the viewer never sees the name swapped for the + * artwork. The fallback is shown meanwhile, because a bar held back waiting on a logo + * is a bar that is late for the thing it was announcing. + */ + private fun bindNextUpIdentity(next: NextEpisode) { + val logo = nextUpLogo ?: return + val fallback = nextUpSeries ?: return + val url = next.logoUrl?.takeIf { it.isNotBlank() } + if (nextUpBoundItemId == next.itemId && nextUpBoundLogoUrl == url) return + nextUpBoundItemId = next.itemId + nextUpBoundLogoUrl = url + fallback.text = next.seriesName.ifBlank { next.title } + logo.clearColorFilter() + logo.visibility = View.GONE + fallback.visibility = View.VISIBLE + if (url == null) return + logo.load(url) { + crossfade(false) + listener( + onSuccess = { _, result -> + // An Emby title treatment is commonly black-on-transparent, which on + // this wash is an invisible heading. Same judgement, same helper, as + // the ident in the opposite corner. + makeLogoVisibleOnDarkBackground(logo, result.drawable) + logo.visibility = View.VISIBLE + fallback.visibility = View.GONE + }, + onError = { _, _ -> + logo.visibility = View.GONE + fallback.visibility = View.VISIBLE + }, + ) + } + } + private fun showNextUp(next: NextEpisode) { val banner = nextUpBanner ?: return + bindNextUpIdentity(next) if (banner.isVisible) return - - banner.findViewById(R.id.player_next_up_title).text = - next.title.ifBlank { next.seriesName } - val meta = listOfNotNull( - next.episodeCode, - next.seriesName.takeIf { it.isNotBlank() && next.title.isNotBlank() }, - ).joinToString(" · ") - banner.findViewById(R.id.player_next_up_meta).apply { - text = meta - visibility = if (meta.isBlank()) View.GONE else View.VISIBLE - } - banner.findViewById(R.id.player_next_up_image).load(next.imageUrl) { - crossfade(true) - } - banner.alpha = 0f banner.visibility = View.VISIBLE - banner.post { - zoomVideoForNextUp() - banner.animate() - .alpha(1f) - .setDuration(NEXT_UP_ANIMATION_MS) - .setInterpolator(DecelerateInterpolator()) - .start() - playerView?.hideController() - banner.findViewById(R.id.player_next_up_play).requestFocus() - } + banner.animate() + .alpha(1f) + .setDuration(NEXT_UP_ANIMATION_MS) + .setInterpolator(DecelerateInterpolator()) + .start() } private fun hideNextUp() { @@ -3758,37 +3826,17 @@ class PlayerActivity : ComponentActivity() { banner.alpha = 1f } .start() - restoreVideoAfterNextUp() } + /** + * Refuses the offer. Back is how it is reached, which is the same contract every other + * overlay in this player has — one press per level, and the key that would otherwise + * leave the programme spends itself on the thing that appeared over it instead. + */ private fun dismissNextUp() { nextUpDismissed = true nextUpJob?.cancel() hideNextUp() - playerView?.requestFocus() - } - - private fun zoomVideoForNextUp() { - val view = playerView ?: return - view.animate() - .scaleX(NEXT_UP_VIDEO_SCALE) - .scaleY(NEXT_UP_VIDEO_SCALE) - .translationX(-view.width * NEXT_UP_VIDEO_SHIFT_X) - .translationY(-view.height * NEXT_UP_VIDEO_SHIFT_Y) - .setDuration(NEXT_UP_ANIMATION_MS) - .setInterpolator(DecelerateInterpolator()) - .start() - } - - private fun restoreVideoAfterNextUp() { - playerView?.animate() - ?.scaleX(1f) - ?.scaleY(1f) - ?.translationX(0f) - ?.translationY(0f) - ?.setDuration(NEXT_UP_ANIMATION_MS) - ?.setInterpolator(DecelerateInterpolator()) - ?.start() } // --- Closing credits ---------------------------------------------------------------- @@ -4512,7 +4560,10 @@ class PlayerActivity : ComponentActivity() { playerView?.isControllerFullyVisible != true && !castPanelVisible.value && subtitleOverlay?.isVisible != true && - nextUpBanner?.isVisible != true && + // The next-up bar is deliberately absent from this list. It takes no focus and + // holds no button, so the centre key still means pause while it is up — the + // whole point of the compact bar is that the remote goes on meaning what it + // meant a moment before it appeared. // The pane's Play button holds focus while it is up, and the centre key is how a // remote presses what it is focused on. creditsView?.isVisible != true && @@ -6142,9 +6193,15 @@ class PlayerActivity : ComponentActivity() { private const val NEXT_EPISODE_PREVIEW_LEAD_MS = 120_000L private const val NEXT_EPISODE_PREVIEW_STARTUP_TIMEOUT_MS = 8_000L private const val NEXT_UP_ANIMATION_MS = 260L - private const val NEXT_UP_VIDEO_SCALE = 0.58f - private const val NEXT_UP_VIDEO_SHIFT_X = 0.18f - private const val NEXT_UP_VIDEO_SHIFT_Y = 0.15f + + /** + * The green the bar's eyebrow and countdown arc share with the rest of Memby. + * + * Written out rather than built with `Color.rgb`, because this companion is + * initialised by plain JUnit tests that have no Android framework under them and + * every android.graphics call from one throws. + */ + private val NEXT_UP_ACCENT = 0xFF69CD61.toInt() private const val NEXT_UP_IMAGE_PREFETCH_WIDTH = 640 private const val NEXT_UP_IMAGE_PREFETCH_HEIGHT = 360 diff --git a/app/src/main/java/com/ponzischeme89/memby/ui/player/SkipIntroCountdownView.kt b/app/src/main/java/com/ponzischeme89/memby/ui/player/SkipIntroCountdownView.kt index c823008..e5b24a4 100644 --- a/app/src/main/java/com/ponzischeme89/memby/ui/player/SkipIntroCountdownView.kt +++ b/app/src/main/java/com/ponzischeme89/memby/ui/player/SkipIntroCountdownView.kt @@ -1,80 +1,28 @@ package com.ponzischeme89.memby.ui.player import android.content.Context -import android.graphics.Canvas import android.graphics.Color -import android.graphics.Paint -import android.graphics.RectF -import android.graphics.Typeface import android.util.AttributeSet -import android.view.View import com.ponzischeme89.memby.R -import com.ponzischeme89.memby.ui.theme.membyTypeface -import kotlin.math.ceil -import kotlin.math.min /** - * The ring on the skip-intro button: how long is left to press it, drawn as a draining arc - * with the figure inside. + * The ring on the skip-intro button: how long is left to press it. * - * Like [PrerollCountdownView] it runs no animator of its own. PlayerActivity advances it - * from the playhead, which is what keeps it honest — pausing during the titles holds the - * ring where it is, and seeking moves it to wherever the film now is, neither of which a - * timer counting wall-clock seconds could do. - * - * It takes its colours from its own drawable state rather than from a setter. The button - * around it is a state-list pill — green with white text, white with dark text once - * focused — so the ring has to change with it or it disappears into the fill the moment - * somebody's remote reaches it. `duplicateParentState` in the layout is what feeds that - * state down; without it this draws focused colours never. + * The drawing is [CountdownRingView]'s, shared with the next-up overlay. What is this + * class's own is the one thing that differs — it takes its colours from its own drawable + * state rather than from a setter. The button around it is a state-list pill — green with + * white text, white with dark text once focused — so the ring has to change with it or it + * disappears into the fill the moment somebody's remote reaches it. `duplicateParentState` + * in the layout is what feeds that state down; without it this draws focused colours never. */ class SkipIntroCountdownView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, -) : View(context, attrs, defStyleAttr) { +) : CountdownRingView(context, attrs, defStyleAttr) { - private val density = resources.displayMetrics.density - private val ringBounds = RectF() - private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - style = Paint.Style.STROKE - strokeCap = Paint.Cap.ROUND - strokeWidth = 2.5f * density - } - private val progressPaint = Paint(trackPaint) - private val figurePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - textAlign = Paint.Align.CENTER - typeface = context.membyTypeface(Typeface.BOLD) - } - - private var figure = "" - private var progress = 1f - - /** - * How much of the offer is left, and how long it ran for. - * - * Redraws only when the drawn result would actually differ. This is advanced several - * times a second for two minutes at a stretch, and a two-minute ring moves by a - * fraction of a degree per tick — invalidating on every one of them would be a couple - * of hundred pointless draws per episode on a box that has a decoder to feed. - */ - fun setRemaining(remainingMs: Long, totalMs: Long) { - val remaining = remainingMs.coerceAtLeast(0L) - val nextFigure = formatRemaining(remaining) - val nextProgress = if (totalMs > 0L) { - (remaining.toFloat() / totalMs.toFloat()).coerceIn(0f, 1f) - } else { - 0f - } - // A degree is about the smallest movement worth a redraw; below that the arc lands - // on the same pixels. - val moved = kotlin.math.abs(nextProgress - progress) * 360f >= 1f - if (nextFigure == figure && !moved) return - figure = nextFigure - progress = nextProgress - contentDescription = context.getString(R.string.player_skip_intro_countdown, nextFigure) - invalidate() - } + override fun describe(figure: String): CharSequence = + context.getString(R.string.player_skip_intro_countdown, figure) override fun drawableStateChanged() { super.drawableStateChanged() @@ -85,61 +33,19 @@ class SkipIntroCountdownView @JvmOverloads constructor( // The unspent part of the ring has to stay visible without competing with the arc. // Dark ink on the focused white pill needs more of itself than white does on green, // where the fill is already doing half the separating. - trackPaint.color = Color.argb( - if (focused) 92 else 72, - Color.red(ink), - Color.green(ink), - Color.blue(ink), + setInk( + track = Color.argb( + if (focused) 92 else 72, + Color.red(ink), + Color.green(ink), + Color.blue(ink), + ), + arc = ink, + figure = ink, ) - progressPaint.color = ink - figurePaint.color = ink - invalidate() } - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - val strokeInset = trackPaint.strokeWidth / 2f - val diameter = min(width, height).toFloat() - val left = (width - diameter) / 2f + strokeInset - val top = (height - diameter) / 2f + strokeInset - ringBounds.set( - left, - top, - left + diameter - trackPaint.strokeWidth, - top + diameter - trackPaint.strokeWidth, - ) - canvas.drawOval(ringBounds, trackPaint) - if (progress > 0f) { - // Anticlockwise from the top, so the ring empties the way a clock hand would - // sweep back rather than filling up as the thing it measures runs out. - canvas.drawArc(ringBounds, -90f, -360f * progress, false, progressPaint) - } - if (figure.isEmpty()) return - figurePaint.textSize = figureTextSize(diameter, figure.length) - val baseline = height / 2f - (figurePaint.ascent() + figurePaint.descent()) / 2f - canvas.drawText(figure, width / 2f, baseline, figurePaint) - } - - /** - * The figure has to fit inside the ring, and "1:58" is twice the width of "12". Sizing - * from the string's own length is what stops a two-minute opening printing over its own - * arc — an intro is commonly long enough to be counted in minutes, so this is the - * ordinary case rather than the edge one. - */ - private fun figureTextSize(diameter: Float, characters: Int): Float = - diameter * if (characters >= 4) 0.30f else 0.42f - private companion object { val FOCUSED_INK = Color.rgb(11, 14, 17) } } - -/** - * "1:58" over a minute, "58" under it. Never "0:58": a bare figure is read at a glance in a - * ring this size, and the colon is only worth its width once there are minutes to separate. - */ -internal fun formatRemaining(remainingMs: Long): String { - val seconds = ceil(remainingMs.coerceAtLeast(0L) / 1_000.0).toInt() - if (seconds < 60) return seconds.toString() - return "${seconds / 60}:${(seconds % 60).toString().padStart(2, '0')}" -} diff --git a/app/src/main/res/drawable/next_up_banner_background.xml b/app/src/main/res/drawable/next_up_banner_background.xml index 1e64680..7723299 100644 --- a/app/src/main/res/drawable/next_up_banner_background.xml +++ b/app/src/main/res/drawable/next_up_banner_background.xml @@ -1,9 +1,15 @@ + - - + diff --git a/app/src/main/res/drawable/next_up_divider.xml b/app/src/main/res/drawable/next_up_divider.xml new file mode 100644 index 0000000..832fab4 --- /dev/null +++ b/app/src/main/res/drawable/next_up_divider.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/layout/player_next_up_banner.xml b/app/src/main/res/layout/player_next_up_banner.xml index ef9284b..a9ffc26 100644 --- a/app/src/main/res/layout/player_next_up_banner.xml +++ b/app/src/main/res/layout/player_next_up_banner.xml @@ -1,6 +1,16 @@ - + + android:baselineAligned="false" + android:focusable="false" + android:gravity="center_vertical" + android:orientation="horizontal" + android:paddingStart="22dp" + android:paddingTop="13dp" + android:paddingEnd="22dp" + android:paddingBottom="13dp"> - - - + + android:layout_height="34dp"> - + - + + - + + android:orientation="vertical"> -