@xenon-device-management/xenon 2.13.0 → 2.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +223 -488
- package/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-CzbFYtPI.js → AnnotationOverlay-BXruClDk.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-CA5KFHHJ.js → ApiKeyGate-qTS4LPLC.js} +1 -1
- package/lib/public/assets/{BugReportButton-D-DSGg8C.js → BugReportButton-BoGhlI7F.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-paDEFU4B.js → DeviceMosaicView-8J0oYqMM.js} +1 -1
- package/lib/public/assets/{EmptyState-BE_5f8Vw.js → EmptyState-Cr17PdDv.js} +1 -1
- package/lib/public/assets/{FieldGroup-DqMf7Rf3.js → FieldGroup-Bvn08svv.js} +1 -1
- package/lib/public/assets/{FilterMenu-BQY75Su3.js → FilterMenu-DDSO8aDf.js} +1 -1
- package/lib/public/assets/{Menu-BiW3os9u.js → Menu-B-Qn2X3T.js} +1 -1
- package/lib/public/assets/{Modal-DfawTxih.js → Modal-Cg9V5JYU.js} +1 -1
- package/lib/public/assets/{RecordingPage-BvemyRiy.js → RecordingPage-BTUs9VTW.js} +1 -1
- package/lib/public/assets/{RecordingsPage-CXfw7wpd.js → RecordingsPage-DYcfxITK.js} +2 -2
- package/lib/public/assets/{SegmentedControl-B7m9kpom.js → SegmentedControl-Zy7suhT5.js} +1 -1
- package/lib/public/assets/{SettingCard-Cw6lDD0l.js → SettingCard-CWYmycoe.js} +1 -1
- package/lib/public/assets/{Table-DlEuFgv-.js → Table-io9fwa98.js} +1 -1
- package/lib/public/assets/{activity-CnLvNwae.js → activity-Bdf_uIWl.js} +1 -1
- package/lib/public/assets/ai-settings-Bce5SJob.js +21 -0
- package/lib/public/assets/{api-keys-DphSVzBr.js → api-keys-CPqtVpm3.js} +1 -1
- package/lib/public/assets/{apps-vUfu15lX.js → apps-BgZan0H9.js} +2 -2
- package/lib/public/assets/{arrow-left-CLQWAiCY.js → arrow-left-B27ckB31.js} +1 -1
- package/lib/public/assets/{arrow-right-K6A5e_Ee.js → arrow-right-DOF8ITCz.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DUA_lq6f.js → arrow-up-right-eFG5KTFS.js} +1 -1
- package/lib/public/assets/{auth-shell-BYRHcZd6.js → auth-shell-DlC3_27j.js} +2 -2
- package/lib/public/assets/{builds-page-C0fTvIG8.js → builds-page-BuXpPfKo.js} +1 -1
- package/lib/public/assets/{button-BxnHCCsX.js → button-fIW8owMI.js} +1 -1
- package/lib/public/assets/{calendar-CqVsn28A.js → calendar-WNjmrF-a.js} +1 -1
- package/lib/public/assets/{check-R6V3bot0.js → check-BdTh1Sq7.js} +1 -1
- package/lib/public/assets/{chevron-right-BA2Fntu-.js → chevron-right-B4BKlsqk.js} +1 -1
- package/lib/public/assets/{circle-check-D4EzyGzs.js → circle-check-zJU80_EF.js} +1 -1
- package/lib/public/assets/{circle-x-Cf6AF2B2.js → circle-x-5Uwquo0h.js} +1 -1
- package/lib/public/assets/{clock-EZfet0sS.js → clock-BtUprDsZ.js} +1 -1
- package/lib/public/assets/{copy-Bc6P8v9V.js → copy-DGSbZI3u.js} +1 -1
- package/lib/public/assets/device-explorer-C-KnhMHI.js +279 -0
- package/lib/public/assets/{download-Bd8OGLzi.js → download-WG6l7BgD.js} +1 -1
- package/lib/public/assets/{forgot-password-CoE7Ugtx.js → forgot-password-DnudqFFo.js} +1 -1
- package/lib/public/assets/{index-BTYMAqNW.css → index-Bxoxt9sV.css} +1 -1
- package/lib/public/assets/{index-CHOr4JCs.js → index-DI1fGIyj.js} +43 -43
- package/lib/public/assets/{index-BiL0Enl_.js → index-HDQVe1gi.js} +1 -1
- package/lib/public/assets/{input-CJ8z3IO5.js → input-BjDRKCea.js} +1 -1
- package/lib/public/assets/{line-chart-CPK0ObjV.js → line-chart-C0zdgd0v.js} +1 -1
- package/lib/public/assets/{list-checks-mXGHUVbB.js → list-checks-BdW5CjM8.js} +1 -1
- package/lib/public/assets/{lock-BSXM1xvq.js → lock-Dy3L4I8E.js} +1 -1
- package/lib/public/assets/{login-Ct2iW066.js → login-BHbY9CLG.js} +1 -1
- package/lib/public/assets/maintenance-settings-zDVlOMVK.js +6 -0
- package/lib/public/assets/{monitor-D8WQK8md.js → monitor-8xjkeHHi.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-Cw2tWzDX.js → mouse-pointer-2-CXdKMXQ8.js} +1 -1
- package/lib/public/assets/{network-Ba_uFxWo.js → network-Cj8JOKON.js} +1 -1
- package/lib/public/assets/{overview-CxrZ7_6k.js → overview-CTg7F0im.js} +1 -1
- package/lib/public/assets/{page-header-Cm1XL49f.js → page-header--aTyJXq6.js} +1 -1
- package/lib/public/assets/{play-C0CuKLXj.js → play-B-FodgHV.js} +1 -1
- package/lib/public/assets/{plus-CIIvR_6Z.js → plus-CsIk10Cq.js} +1 -1
- package/lib/public/assets/{profile-page-BZ_e-Bgw.js → profile-page-DkFKkMfl.js} +1 -1
- package/lib/public/assets/{recording-group-store-BWeeNs2_.js → recording-group-store-WVM3hoXn.js} +1 -1
- package/lib/public/assets/{reset-password-lfNz8Rm1.js → reset-password-I2-zqLlI.js} +1 -1
- package/lib/public/assets/{runbook-page-Cnec3VSx.js → runbook-page-DJTVCJX7.js} +1 -1
- package/lib/public/assets/{select-CZuCnold.js → select-DvCSv25z.js} +1 -1
- package/lib/public/assets/{selector-detail-redirect-BG3pxFLw.js → selector-detail-redirect-BLVfMJs_.js} +1 -1
- package/lib/public/assets/{selector-health-page-DsyL58tb.js → selector-health-page-oqadRokn.js} +2 -2
- package/lib/public/assets/session-detail-page-CGn55Pvl.js +4 -0
- package/lib/public/assets/settings-MsVU2ukE.js +6 -0
- package/lib/public/assets/{stat-tile-DFhv8JoI.js → stat-tile-DV9Ix-4O.js} +1 -1
- package/lib/public/assets/{tablet-68J5f6zh.js → tablet-6b_SEN2X.js} +1 -1
- package/lib/public/assets/{teams-eHSyEpbD.js → teams-BjZyfuHh.js} +1 -1
- package/lib/public/assets/{trash-2-CcSsv4fS.js → trash-2-CTZpPSdd.js} +1 -1
- package/lib/public/assets/{upload-BYf26K71.js → upload-P3XzwQUb.js} +1 -1
- package/lib/public/assets/{use-builds-data-CHW-MhaP.js → use-builds-data-1cOCMf-z.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-DozShXkk.js → use-password-reset-mode-DgF9w088.js} +1 -1
- package/lib/public/assets/{users-CcSQRoK9.js → users-CiNTKUiz.js} +1 -1
- package/lib/public/assets/{users-BpYnpOl2.js → users-DMF9Mk5H.js} +1 -1
- package/lib/public/assets/{video-off-dFKgps73.js → video-off-CHp1KU_T.js} +1 -1
- package/lib/public/assets/webhook-settings-DiS8geIm.js +1 -0
- package/lib/public/assets/{zap-COC2tZaH.js → zap-DRz0tDq5.js} +1 -1
- package/lib/public/index.html +2 -2
- package/lib/schema.json +0 -25
- package/lib/src/app/routers/control.js +19 -4
- package/lib/src/app/routers/streamStartConflict.js +3 -0
- package/lib/src/data-service/device-service.js +40 -0
- package/lib/src/data-service/device-store.js +4 -1
- package/lib/src/data-service/prisma-store.js +2 -1
- package/lib/src/device-managers/ios/WDAClient.js +128 -16
- package/lib/src/device-utils.js +63 -13
- package/lib/src/middleware/deviceAccessGuard.js +63 -9
- package/lib/src/services/ServerManager.js +3 -0
- package/lib/src/services/SessionLifecycleService.js +16 -4
- package/lib/src/services/device-access/SessionOwnerResolver.js +26 -0
- package/lib/src/services/device-access/deviceAccessPolicy.js +38 -6
- package/lib/src/services/device-access/leaseHold.js +26 -0
- package/lib/src/services/device-access/ticketActorAccess.js +6 -0
- package/lib/src/services/lease/activeLeases.js +37 -0
- package/lib/src/services/recording/busy-precheck.js +24 -3
- package/lib/test/unit/device-access-lease.spec.js +270 -0
- package/lib/test/unit/lease/allocate-lease-branch.spec.js +22 -0
- package/lib/test/unit/lease/session-lease-proof.spec.js +59 -0
- package/lib/test/unit/lease-held-phone.spec.js +196 -0
- package/lib/test/unit/partial-config-file.spec.js +100 -0
- package/lib/test/unit/schema-required-args.spec.js +21 -16
- package/lib/test/unit/wda-client-session.spec.js +128 -0
- package/lib/test/unit/wda-clipboard-write.spec.js +135 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/schema.json +0 -25
- package/scripts/dev/readme-screenshots.js +231 -0
- package/scripts/generate-types-from-schema.js +6 -0
- package/scripts/lib/always-present-plugin-args.js +39 -0
- package/lib/public/assets/ai-settings-D7Te8o-a.js +0 -21
- package/lib/public/assets/device-explorer-BFXpRq6d.js +0 -279
- package/lib/public/assets/maintenance-settings-DaOs00P6.js +0 -6
- package/lib/public/assets/session-detail-page-ZZyGM413.js +0 -4
- package/lib/public/assets/settings-DGZ7B_AR.js +0 -6
- package/lib/public/assets/webhook-settings-DD1XjiOD.js +0 -1
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{r as x,X as k,j as t,e as Z,f as Ce,t as F,y as Re,R as Te,p as $e,i as Ae,u as Ee}from"./index-DI1fGIyj.js";import{C as De,d as Le,p as be,e as Me,r as ge,f as Oe,j as je,k as W,c as Fe,h as ee,u as Pe,b as Be,q as He}from"./use-builds-data-1cOCMf-z.js";import{A as Ie}from"./arrow-left-B27ckB31.js";import{C as V}from"./chevron-right-B4BKlsqk.js";import{s as Ue}from"./labels-C21SA4JG.js";import{B as Ve,S as Ne,P as _}from"./BugReportButton-BoGhlI7F.js";import{C as Je}from"./circle-x-5Uwquo0h.js";import{C as We}from"./circle-check-zJU80_EF.js";import{S as L}from"./stat-tile-DV9Ix-4O.js";import{C as z}from"./copy-DGSbZI3u.js";import{A as ze}from"./arrow-up-right-eFG5KTFS.js";import{A as Ke}from"./arrow-right-DOF8ITCz.js";import{f as Ge,a as te,C as ye,b as Xe,s as Ye,L as qe,n as re}from"./line-chart-C0zdgd0v.js";import{E}from"./EmptyState-Cr17PdDv.js";import{A as Qe}from"./activity-Bdf_uIWl.js";import{V as Ze}from"./video-off-CHp1KU_T.js";import{D as we}from"./download-WG6l7BgD.js";import{T as et,a as tt,b as se,c as T,d as rt,e as $}from"./Table-io9fwa98.js";import{M as st}from"./Modal-Cg9V5JYU.js";import{N as at}from"./network-Cj8JOKON.js";import"./time-D-gjmE1o.js";import"./index-HDQVe1gi.js";const nt=4e3,ae=e=>!!e&&typeof e=="object"&&typeof e.id=="string"&&typeof e.status=="string";function it(e){const[r,s]=x.useState(null),[a,n]=x.useState([]),[o,l]=x.useState([]),[i,d]=x.useState([]),[m,b]=x.useState([]),[f,v]=x.useState(!0),[j,N]=x.useState(null),[y,w]=x.useState(!1),[A,C]=x.useState(0),g=x.useRef(null);x.useEffect(()=>{if(!e){s(null),n([]),l([]),d([]),b([]),w(!1),v(!1);return}let u=!0;return g.current!==e&&v(!0),Promise.all([k.getSession(e),k.getSessionLogs(e),k.getDeviceLogs(e),k.getDebugLogs(e),k.getProfilingData(e).catch(()=>[])]).then(([p,h,R,D,q])=>{if(!u)return;const Q=ae(p);s(Q?p:null),Q?(g.current=e,w(!1),N(null)):(w(!!p&&typeof p=="object"&&(p.error===!0||typeof p.message=="string")),N(typeof(p==null?void 0:p.message)=="string"?p.message:"Session not found")),n(Array.isArray(h)?h:[]),l(Array.isArray(R)?R:[]),d(Array.isArray(D)?D:[]),b(Array.isArray(q)?q:[])}).catch(p=>{u&&N(p instanceof Error?p.message:String(p))}).finally(()=>{u&&v(!1)}),()=>{u=!1}},[e,A]);const c=(r==null?void 0:r.status)==="running";return x.useEffect(()=>{if(!e||!c)return;let u=!0;const p=setInterval(async()=>{try{const[h,R]=await Promise.all([k.getSession(e),k.getSessionLogs(e)]);if(!u||!ae(h))return;s(h),Array.isArray(R)&&n(R),h.status!=="running"&&C(D=>D+1)}catch{}},nt);return()=>{u=!1,clearInterval(p)}},[e,c]),{session:r,sessionLogs:a,deviceLogs:o,debugLogs:i,profiling:m,loading:f,error:j,notFound:y,refresh:()=>C(u=>u+1)}}const ne=({buildId:e,buildName:r,sessionId:s})=>t.jsxs("nav",{"aria-label":"Breadcrumb",className:"flex items-center gap-2 px-4 py-2 border-b border-[var(--border)] bg-[var(--surface)]",children:[t.jsxs(Z,{to:"/builds",className:"inline-flex items-center gap-1.5 text-xs text-[var(--text-muted)] hover:text-[var(--text)] transition-colors",children:[t.jsx(Ie,{className:"h-3.5 w-3.5"}),"Sessions"]}),t.jsx(V,{className:"h-3 w-3 text-[var(--text-dim)]"}),r&&t.jsxs(t.Fragment,{children:[t.jsx(Z,{to:`/builds/${e}`,className:"text-[11px] text-[var(--color-accent)] hover:underline",children:r}),t.jsx(V,{className:"h-3 w-3 text-[var(--text-dim)]"})]}),t.jsxs("span",{className:"font-mono text-[11px] text-[var(--text-muted)]",title:s,children:["#",s.slice(0,8)]})]}),ot={passed:{label:"Passed",Icon:We,cls:"text-[var(--color-success)]"},failed:{label:"Failed",Icon:Je,cls:"text-[var(--color-danger)]"},running:{label:"Running",Icon:Ce,cls:"text-[var(--color-warning)]"},other:{Icon:De,cls:"text-[var(--text-dim)]"}};function K(e){const r=Fe(e),s=ot[r];return{bucket:r,...s,label:s.label??Ue(e||"unknown")}}const lt=({session:e})=>{var o,l;const r=K(e.status),s=Le(e),a=[be(e),e.device_version].filter(i=>i&&i!=="—").join(" "),n=[[Me(e),a].filter(Boolean).join(" · "),ge(e.ranOn),(o=e.owner)==null?void 0:o.name,Oe(e.startTime),je(W(e))].filter(i=>i&&i!=="—");return t.jsxs("header",{className:"flex items-start justify-between gap-4 px-4 py-4 border-b border-[var(--border)] bg-[var(--surface)]",children:[t.jsxs("div",{className:"min-w-0",children:[t.jsxs("div",{className:"flex items-center gap-2 min-w-0",children:[t.jsxs("span",{className:`inline-flex items-center gap-1.5 text-xs font-medium shrink-0 ${r.cls}`,children:[t.jsx(r.Icon,{className:`h-3.5 w-3.5 ${r.bucket==="running"?"animate-spin":""}`,"aria-hidden":"true"}),r.label]}),t.jsx("h1",{className:`text-lg font-semibold truncate ${s.source==="name"?"text-[var(--text)]":"text-[var(--text-muted)]"}`,title:s.source==="name"?s.text:`${s.text}
|
|
2
|
+
Unnamed session. Set xe:options.name in its capabilities to name it.`,children:s.text})]}),t.jsx("p",{"data-testid":"outcome-meta",className:"mt-1 text-xs text-[var(--text-muted)] tabular-nums truncate",title:((l=e.owner)==null?void 0:l.email)??void 0,children:n.join(" · ")})]}),t.jsx("div",{className:"shrink-0",children:t.jsx(Ve,{sessionId:e.id,mode:"full",variant:"inline"})})]})};function ct(e,r=8){if(!e)return[];const s=e.split(/\r?\n/),a=[];for(const n of s){const o=n.trim();if(/^at\s+/.test(o)&&(a.push(o),a.length>=r))break}return a}function G(e){return e?e.split(/[_\s]+/).filter(Boolean).map(r=>r.charAt(0).toUpperCase()+r.slice(1).toLowerCase()).join(" "):""}function ie(e){if(!e)return{};try{const r=JSON.parse(e);return r&&typeof r=="object"&&!Array.isArray(r)?r:{}}catch{return{}}}function oe(e,r=200){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return String(e);try{const s=JSON.stringify(e);return s.length>r?s.slice(0,r)+"…":s}catch{return String(e)}}const P=e=>e.command_name||e.title||"command",J=e=>{const r=e.createdAt?Date.parse(e.createdAt):NaN;return Number.isFinite(r)?r:0},X=e=>[...e].sort((r,s)=>J(r)-J(s));function B(e){return e.is_success===!1||e.is_error===!0}function ke(e){const r=[];let s=null;for(const a of X(e))a.is_healed&&a.healing_tier&&!r.includes(a.healing_tier)&&r.push(a.healing_tier),typeof a.duration=="number"&&(!s||a.duration>s.ms)&&(s={name:P(a),ms:a.duration});return{total:e.length,failed:e.filter(B).length,healed:e.filter(a=>a.is_healed).length,healedTiers:r,slowest:s,screenshots:e.filter(a=>a.screenshot).length}}const le=(e,r)=>r?e?`${e}=${r}`:r:null;function dt(e){return X(e.filter(r=>r.is_healed)).map(r=>({id:r.id,command:P(r),original:le(r.original_strategy,r.original_selector),healed:le(r.healed_strategy,r.healed_selector),tier:r.healing_tier??null,confidence:typeof r.healing_confidence=="number"?Math.round(r.healing_confidence*100):null,at:r.createdAt}))}function xt(e){var r;if(!e.response)return null;try{const s=(r=JSON.parse(e.response))==null?void 0:r.value;if(!s||typeof s!="object")return null;const a=typeof s.message=="string"?s.message.trim():"";return a||(typeof s.error=="string"&&s.error?s.error:null)}catch{return null}}function mt(e){if(e.length===0)return{spanMs:0,bars:[]};const r=e.map(n=>{const o=J(n),l=typeof n.duration=="number"&&n.duration>0?n.duration:0;return{l:n,start:o-l,end:o,durationMs:l}}),s=Math.min(...r.map(n=>n.start)),a=Math.max(...r.map(n=>n.end));return{spanMs:Math.max(1,a-s),bars:r.sort((n,o)=>n.start-o.start).map(({l:n,start:o,durationMs:l})=>({id:n.id,label:P(n),startMs:o-s,durationMs:l,failed:B(n),healed:!!n.is_healed}))}}function ut(e){return X(e.filter(r=>r.screenshot)).map(r=>({id:r.id,path:r.screenshot,command:P(r),at:r.createdAt,failed:B(r)}))}function O(e){if(e==null||!Number.isFinite(e)||e<0)return"—";if(e<1e3)return`${Math.round(e)}ms`;if(e<6e4)return`${(e/1e3).toFixed(1)}s`;const r=Math.round(e/1e3);return`${Math.floor(r/60)}m ${r%60}s`}const ft=({session:e,commands:r})=>{var l,i;const s=K(e.status),a=ke(r),n=je(W(e)),o=s.bucket==="failed"?G(e.failure_category)||"No category recorded":s.bucket==="running"?`Running for ${n}`:`Ran ${n}`;return t.jsxs("div",{className:"grid grid-cols-4 gap-3",children:[t.jsx(L,{label:"Result",value:s.label,valueCls:s.cls,note:o,noteTone:s.bucket==="failed"?"bad":"neutral"}),t.jsx(L,{label:"Commands",value:a.total,note:a.failed>0?`${a.failed} failed`:"None failed",noteTone:a.failed>0?"bad":"neutral"}),t.jsx(L,{label:"Self-healing",value:a.healed>0?`${a.healed} healed`:"None",note:a.healed>0?a.healedTiers.join(", ")||"Tier not recorded":"No selector needed healing"}),t.jsx(L,{label:"Slowest command",value:O((l=a.slowest)==null?void 0:l.ms),note:((i=a.slowest)==null?void 0:i.name)??"No timings recorded"})]})};function ht(e){const r=e.filter(B),s=r[r.length-1];return s?{command:s.command_name||s.title||"command",message:xt(s)}:null}function pt(e,r,s,a,n){var m;const o=G(e.failure_category),l=((m=e.device_name)==null?void 0:m.trim())||"Unknown Device",i=e.device_version?` ${e.device_version}`:"",d=n?`${n.command}${n.message?`: ${n.message}`:""}`:"(no failed command recorded)";return[`**Session:** ${e.id}`,`**Build:** ${r} (#${s})`,`**Device:** ${l} (${e.node_id||"unknown-node"}) · ${e.device_platform||"unknown"}${i}`,`**Duration:** ${a}`,`**Failure category:** ${o||"(uncategorized)"}`,`**Failure reason:** ${e.failure_reason||"(not set)"}`,`**First failed command:** ${d}`,...e.ai_analysis?[`**AI analysis:** ${e.ai_analysis}`]:[]].join(`
|
|
3
|
+
`)}function vt({text:e}){return t.jsx(t.Fragment,{children:e.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).map((r,s)=>/^\*\*[^*]+\*\*$/.test(r)?t.jsx("strong",{children:r.slice(2,-2)},s):/^`[^`]+`$/.test(r)?t.jsx("code",{className:"font-mono text-[11px] px-1 rounded bg-[var(--surface-2)]",children:r.slice(1,-1)},s):t.jsx(Te.Fragment,{children:r},s))})}const M=({children:e})=>t.jsx("div",{className:"text-[11px] font-semibold text-[var(--text-dim)] mb-1",children:e}),bt=({session:e,buildName:r,buildId:s,commands:a,durationText:n})=>{var y;const{toast:o}=F(),[l,i]=x.useState(!1),d=G(e.failure_category),m=ht(a),b=ct(e.failure_reason),f=(y=e.ai_analysis)==null?void 0:y.trim(),v=!!f&&f.length>480,j=async()=>{const w=pt(e,r,s,n,m);try{await navigator.clipboard.writeText(w),o("Failure report copied","success")}catch{o("Clipboard unavailable","error")}},N=e.failure_category?`/xenon/runbooks/${encodeURIComponent(e.failure_category.toLowerCase())}`:void 0;return t.jsxs("section",{className:"rounded-lg border border-[var(--red)]/30 bg-[var(--surface)] overflow-hidden",children:[t.jsxs("header",{className:"flex items-center justify-between gap-4 px-4 py-3 border-b border-[var(--border)] bg-[var(--red)]/5",children:[t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(Re,{className:"h-4 w-4 text-[var(--red)]","aria-hidden":"true"}),t.jsx("h2",{className:"text-sm font-semibold text-[var(--red)]",children:"Why it failed"}),d&&t.jsx("span",{className:"text-[11px] text-[var(--text-dim)] ml-2",children:d})]}),t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsxs("button",{type:"button",onClick:j,className:"inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-[var(--border)] text-xs text-[var(--text)] hover:border-[var(--border-strong)] hover:bg-[var(--surface-2)] transition-colors",children:[t.jsx(z,{className:"h-3 w-3"}),"Copy"]}),t.jsxs("a",{...N?{href:N,target:"_blank",rel:"noreferrer"}:{"aria-disabled":!0,onClick:w=>w.preventDefault()},className:`inline-flex items-center gap-1.5 h-7 px-2.5 rounded-md border border-[var(--border)] text-xs text-[var(--text)] hover:border-[var(--border-strong)] hover:bg-[var(--surface-2)] transition-colors ${N?"":"opacity-50 cursor-not-allowed"}`,title:N?"Open runbook in new tab":"No runbook — failure category not set",children:[t.jsx(ze,{className:"h-3 w-3"}),"Open runbook"]})]})]}),t.jsxs("div",{className:"px-4 py-4 space-y-4",children:[t.jsxs("div",{children:[t.jsx(M,{children:"Reason"}),t.jsx("div",{className:"text-sm text-[var(--text)] break-words",children:e.failure_reason||t.jsx("span",{className:"text-[var(--text-dim)]",children:"(not set)"})})]}),t.jsxs("div",{children:[t.jsx(M,{children:"First failed command"}),m?t.jsxs("div",{className:"flex items-baseline gap-2 min-w-0 text-sm",children:[t.jsx("span",{className:"font-mono text-xs text-[var(--color-danger)] shrink-0",children:m.command}),m.message&&t.jsx("span",{className:"text-[var(--text-muted)] break-words min-w-0",children:m.message})]}):t.jsx("span",{className:"text-sm text-[var(--text-dim)]",children:"No failed command recorded."})]}),f&&t.jsxs("div",{"data-testid":"ai-analysis",children:[t.jsx(M,{children:t.jsxs("span",{className:"inline-flex items-center gap-1.5",children:[t.jsx(Ne,{className:"h-3 w-3","aria-hidden":"true"}),"AI analysis"]})}),t.jsx("div",{className:`text-sm text-[var(--text)] whitespace-pre-wrap break-words ${v&&!l?"line-clamp-6":""}`,children:t.jsx(vt,{text:f})}),v&&t.jsx("button",{type:"button",onClick:()=>i(w=>!w),"aria-expanded":l,className:"mt-1 text-xs text-[var(--color-accent)] hover:underline",children:l?"Show less":"Show all"})]}),b.length>0&&t.jsxs("div",{children:[t.jsx(M,{children:"Stack trace"}),t.jsx("pre",{className:"rounded-md border border-[var(--border)] bg-[var(--bg)] p-3 font-mono text-[11px] leading-relaxed text-[var(--color-accent)] overflow-x-auto",children:b.join(`
|
|
4
|
+
`)})]})]})]})};function gt(e){return e>=1e3?`${(e/1e3).toFixed(1).replace(/\.0$/,"")}k`:String(e)}function H(e){const r=e;return typeof r.message=="string"&&!r.command_name&&!r.title}const jt=/^\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}\.\d+\s+\d+\s+\d+\s+([VDIWEF])\s/,Nt=/^([VDIWEF])\/[^(:]*[(:]/,yt=/<(Error|Fault|Warning)>/;function Se(e){var a,n,o;const r=((a=jt.exec(e))==null?void 0:a[1])??((n=Nt.exec(e))==null?void 0:n[1]);if(r)return r==="E"||r==="F"?"error":r==="W"?"warn":null;const s=(o=yt.exec(e))==null?void 0:o[1];return s?s==="Warning"?"warn":"error":null}function I(e){const r=e.timestamp;if(!r){const l=e.createdAt;return l?I({timestamp:l}):"--:--:--"}const s=typeof r=="number"?new Date(r):new Date(String(r));if(Number.isNaN(s.getTime()))return"--:--:--";const a=String(s.getHours()).padStart(2,"0"),n=String(s.getMinutes()).padStart(2,"0"),o=String(s.getSeconds()).padStart(2,"0");return`${a}:${n}:${o}`}function wt(e){if(H(e)){const s=Se(e.message);return s==="error"?{label:"error",tone:"red"}:s==="warn"?{label:"warn",tone:"amber"}:{label:"",tone:"neutral"}}if(e.is_healed===!0)return{label:String(e.command_name||"healed"),tone:"amber"};if(e.is_success===!1||e.is_error===!0)return{label:String(e.command_name||e.title||"error"),tone:"red"};if(e.is_success===!0)return{label:String(e.command_name||e.title||"ok"),tone:"green"};const r=e.title||e.event||e.command_name;return typeof r=="string"?/stop|fail|error/i.test(r)?{label:r,tone:"red"}:/start|begin|init/i.test(r)?{label:r,tone:"green"}:{label:r,tone:"blue"}:{label:"log",tone:"neutral"}}function kt(e){if(e==null)return"";if(typeof e=="string")try{return JSON.stringify(JSON.parse(e),null,2)}catch{return e}try{return JSON.stringify(e,null,2)}catch{return String(e)}}function St(e){if(!e||typeof e!="string")return[{text:String(e??""),kind:"plain"}];const r=[],s=/("(?:\\.|[^"\\])*")(\s*:)|("(?:\\.|[^"\\])*")|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false)\b|\b(null)\b|([{}[\],])/g;let a=0,n;for(;(n=s.exec(e))!==null;)n.index>a&&r.push({text:e.slice(a,n.index),kind:"plain"}),n[1]&&n[2]!==void 0?(r.push({text:n[1],kind:"key"}),r.push({text:n[2],kind:"punct"})):n[3]?r.push({text:n[3],kind:"string"}):n[4]?r.push({text:n[4],kind:"number"}):n[5]?r.push({text:n[5],kind:"boolean"}):n[6]?r.push({text:n[6],kind:"null"}):n[7]&&r.push({text:n[7],kind:"punct"}),a=s.lastIndex;return a<e.length&&r.push({text:e.slice(a),kind:"plain"}),r}function _t(e){if(H(e))return e.message.split(/\r?\n/,1)[0].trimEnd();const r=e,s=typeof r.title=="string"&&r.title.trim()?r.title.trim():null,a=typeof r.command_name=="string"&&r.command_name.trim()?r.command_name.trim():null;return s??a??"Command"}function ce(e){const r=e.subtitle;return typeof r=="string"&&r.trim()?r.trim():null}function Ct(e,r){return r?e.filter(s=>s.is_success===!1||s.is_error===!0||H(s)&&Se(s.message)==="error"):e}const Rt=({commands:e})=>{const r=dt(e);return r.length===0?null:t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsxs("header",{className:"flex items-center gap-2 px-4 py-3 border-b border-[var(--border)]",children:[t.jsx(Ne,{className:"h-4 w-4 text-[var(--color-warning)]","aria-hidden":"true"}),t.jsx("h2",{className:"text-sm font-semibold text-[var(--text)]",children:"Self-healing"}),t.jsxs("span",{className:"text-[11px] text-[var(--text-muted)]",children:[r.length," selector",r.length===1?"":"s"," healed. Update the test so the next run doesn't need to."]})]}),t.jsxs("table",{className:"w-full table-fixed text-left",children:[t.jsx("thead",{children:t.jsxs("tr",{className:"border-b border-[var(--border)] text-[11px] text-[var(--text-dim)]",children:[t.jsx("th",{className:"px-4 py-2 font-medium w-[88px]",children:"Time"}),t.jsx("th",{className:"px-3 py-2 font-medium w-[128px]",children:"Command"}),t.jsx("th",{className:"px-3 py-2 font-medium",children:"Asked for"}),t.jsx("th",{className:"w-[28px]",children:t.jsx("span",{className:"sr-only",children:"healed to"})}),t.jsx("th",{className:"px-3 py-2 font-medium",children:"Healed to"}),t.jsx("th",{className:"px-3 py-2 font-medium w-[112px]",children:"Tier"}),t.jsx("th",{className:"px-4 py-2 font-medium w-[96px] text-right",children:"Confidence"})]})}),t.jsx("tbody",{children:r.map((s,a)=>t.jsxs("tr",{className:"border-b border-[var(--border)] last:border-b-0 align-top",children:[t.jsx("td",{className:"px-4 py-2 text-xs text-[var(--text-muted)] tabular-nums",children:I({timestamp:s.at??null})}),t.jsx("td",{className:"px-3 py-2 text-xs text-[var(--text)] truncate",children:s.command}),t.jsx("td",{className:"px-3 py-2 font-mono text-[11px] text-[var(--text-muted)] break-all",children:s.original??"—"}),t.jsx("td",{className:"py-2 text-[var(--text-dim)]",children:t.jsx(Ke,{className:"h-3.5 w-3.5","aria-hidden":"true"})}),t.jsx("td",{className:"px-3 py-2 font-mono text-[11px] text-[var(--text)] break-all",children:s.healed??"—"}),t.jsx("td",{className:"px-3 py-2 text-xs text-[var(--text)]",children:s.tier??"—"}),t.jsx("td",{className:"px-4 py-2 text-xs text-[var(--text)] tabular-nums text-right",children:s.confidence===null?"—":`${s.confidence}%`})]},s.id??a))})]})]})},Tt=1e4,de="var(--color-info)",xe="var(--color-accent)",$t="var(--border-strong)";function me(e){return e>=1024?re(e/1024)*1024:re(e)}const ue=e=>{let r=0;for(const s of e)for(const a of s.values)a!==null&&a>r&&(r=a);return r},U=({title:e,series:r,times:s,yMax:a,format:n,ariaLabel:o})=>t.jsxs("section",{"aria-label":e,children:[t.jsxs("div",{className:"mb-2 flex flex-wrap items-baseline gap-x-4 gap-y-1",children:[t.jsx("h3",{className:"text-xs font-semibold text-[var(--text)]",children:e}),r.map(l=>{const i=Ye(l.values);return t.jsxs("span",{className:"flex min-w-0 items-center gap-1.5 text-[11px] tabular-nums text-[var(--text-muted)]",children:[t.jsx("span",{className:"inline-block h-0.5 w-3 shrink-0",style:{background:l.color}}),t.jsx("span",{className:"max-w-[16rem] truncate",title:l.label,children:l.label}),i&&(l.dashed?t.jsx("span",{children:n(i.peak)}):t.jsxs("span",{children:["peak ",n(i.peak)," · avg ",n(i.average)]}))]},l.key)})]}),t.jsx(qe,{times:s,series:r,yMax:a,formatValue:n,ariaLabel:o})]}),At=({sessionId:e,running:r,hasTrace:s})=>{const[a,n]=x.useState(null),[o,l]=x.useState(!1);x.useEffect(()=>{let c=!0;const u=()=>k.getSessionMetrics(e).then(h=>{c&&(n(Xe(h)),l(!0))}).catch(()=>{c&&l(!0)});if(u(),!r)return()=>{c=!1};const p=setInterval(()=>void u(),Tt);return()=>{c=!1,clearInterval(p)}},[e,r]);const i=(a==null?void 0:a.samples)??[],d=x.useMemo(()=>i.map(c=>{var u;return c.t-(((u=i[0])==null?void 0:u.t)??0)}),[i]),m=new Set(i.map(c=>c.app).filter(c=>!!c)),b=m.size>1?"Foreground app":Array.from(m)[0]??(a==null?void 0:a.appId)??"App",f=i.map(c=>c.app??null),v=c=>c>0&&!!f[c]&&!!f[c-1]&&f[c]!==f[c-1],j=[],N=[],y=[];a&&(a.series.deviceCpu&&j.push({key:"deviceCpu",label:"Device",color:de,values:i.map(c=>c.deviceCpu)}),a.series.appCpu&&j.push({key:"appCpu",label:b,color:xe,values:i.map((c,u)=>v(u)?null:c.appCpu),pointLabels:f}),a.series.appMem&&N.push({key:"appMem",label:b,color:xe,values:i.map((c,u)=>v(u)?null:c.appMemMb),pointLabels:f}),a.series.deviceMem&&(y.push({key:"deviceMem",label:"Device used",color:de,values:i.map(c=>c.deviceMemMb)}),y.push({key:"deviceTotal",label:"Device total",color:$t,values:i.map(c=>c.deviceMemTotalMb),dashed:!0})));const w=r&&(a==null?void 0:a.recording)==="off",A=(a==null?void 0:a.recording)==="stopped",C="Recording stopped: the device stopped responding.";let g;return o?w?g=t.jsx(E,{title:"Performance isn't recorded for this session",description:"CPU and memory are recorded for Android devices and iPhones. This session runs on an iOS simulator or on a machine that needs updating, or recording is turned off."}):i.length===0&&!r?g=t.jsx(E,{title:"No performance data for this session",description:"CPU and memory are recorded for Android devices and iPhones. This session ran on an iOS simulator or on a machine that needed updating, before recording was available, or with recording turned off."}):i.length<2?g=t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:A?C:r?"Collecting… The first figures appear within 10 seconds.":"Too few figures were recorded to draw a chart."}):g=t.jsxs("div",{className:"space-y-4",children:[A&&t.jsxs("p",{className:"text-xs text-[var(--color-warning)]",children:[C," The data ends there."]}),(a==null?void 0:a.platform)==="ios"&&t.jsxs("p",{className:"text-xs text-[var(--text-dim)]",children:["On iPhones, only the device's overall CPU is recorded.",s?" For more detail, download the Performance trace under Details.":""]}),j.length>0&&t.jsx(U,{title:"CPU",series:j,times:d,yMax:100,format:Ge,ariaLabel:"CPU over the session"}),N.length>0&&t.jsx(U,{title:"App memory",series:N,times:d,yMax:me(ue(N)),format:te,ariaLabel:"App memory over the session"}),y.length>0&&t.jsx(U,{title:"Device memory",series:y,times:d,yMax:me(ue(y)),format:te,ariaLabel:"Device memory over the session"})]}):g=t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:"Loading performance…"}),t.jsx(ye,{header:t.jsxs("span",{className:"flex items-center gap-2",children:[t.jsx(Qe,{size:14})," Performance"]}),children:g})},Et=({session:e})=>{const r=e.status==="running",s=!!e.video_recording;return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsxs("header",{className:"px-3 py-2 border-b border-[var(--border)] flex items-center justify-between",children:[t.jsx("span",{className:"text-[11px] font-semibold text-[var(--text-dim)]",children:"Recording"}),r&&t.jsxs("span",{className:"inline-flex items-center gap-1.5 text-[11px] text-[var(--red)]",children:[t.jsx("span",{className:"h-1.5 w-1.5 rounded-full bg-[var(--red)] pulse-dot"}),"Live"]})]}),t.jsx("div",{className:"aspect-video bg-[var(--bg)] flex items-center justify-center",children:r&&e.has_live_video?t.jsx("img",{src:k.getSessionLiveVideoUrl(e.id),alt:`Live view of session ${e.id}`,className:"w-full h-full object-contain"}):s?t.jsx("video",{controls:!0,src:k.getAssetUrl(e.video_recording),className:"w-full h-full object-contain"}):t.jsxs("div",{className:"flex flex-col items-center text-center px-6 py-8 gap-2",children:[t.jsx("div",{className:"h-10 w-10 rounded-full bg-[var(--surface-2)] border border-[var(--border)] flex items-center justify-center",children:t.jsx(Ze,{className:"h-5 w-5 text-[var(--text-dim)]"})}),t.jsx("div",{className:"text-sm text-[var(--text)] font-medium",children:"No video available"}),t.jsx("div",{className:"text-xs text-[var(--text-dim)] max-w-xs",children:e.status==="failed"?"Session failed before recording started.":e.video_recording_enabled===!1?"Recording was disabled for this session.":"No recording was captured."})]})})]})},S=({label:e,children:r,title:s,mono:a})=>t.jsxs("div",{className:"flex items-baseline justify-between gap-3 px-3 py-2",children:[t.jsx("dt",{className:"text-xs text-[var(--text-muted)] shrink-0",children:e}),t.jsx("dd",{className:`text-xs text-[var(--text)] text-right min-w-0 break-all ${a?"font-mono text-[11px]":""}`,title:s,children:r})]}),Dt=({session:e,buildName:r})=>{var l,i;const{toast:s}=F(),a=ge(e.ranOn),n=e.performance_trace?k.getAssetUrl(e.performance_trace):"",o=async()=>{try{await navigator.clipboard.writeText(e.id),s("Session ID copied","success")}catch{s("Clipboard unavailable","error")}};return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsx("header",{className:"px-3 py-2 border-b border-[var(--border)]",children:t.jsx("h2",{className:"text-[11px] font-semibold text-[var(--text-dim)]",children:"Details"})}),t.jsxs("dl",{className:"divide-y divide-[var(--border)]",children:[t.jsx(S,{label:"Session ID",mono:!0,children:t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{children:e.id}),t.jsx("button",{type:"button",onClick:o,"aria-label":"Copy session ID",className:"p-0.5 rounded text-[var(--text-dim)] hover:text-[var(--text)]",children:t.jsx(z,{className:"h-3 w-3"})})]})}),r&&t.jsx(S,{label:"Build",children:r}),t.jsx(S,{label:"Device UDID",mono:!0,children:e.device_udid||"—"}),t.jsx(S,{label:"Platform",children:[be(e),e.device_version].filter(d=>d&&d!=="—").join(" ")||"—"}),t.jsx(S,{label:"Ran on",title:e.node_id?`Node ${e.node_id}`:void 0,children:a??"Not known"}),t.jsx(S,{label:"Run by",title:(l=e.owner)==null?void 0:l.email,children:((i=e.owner)==null?void 0:i.name)??"Not recorded"}),t.jsx(S,{label:"Started",children:ee(e.startTime)}),t.jsx(S,{label:"Ended",children:e.endTime?ee(e.endTime):"—"}),e.tags&&t.jsx(S,{label:"Tags",children:e.tags}),n&&t.jsx(S,{label:"iOS trace",children:t.jsxs("a",{href:n,className:"inline-flex items-center gap-1 text-[var(--color-accent)] hover:underline",children:[t.jsx(we,{className:"h-3 w-3","aria-hidden":"true"}),"Performance trace"]})})]})]})},fe={desired:"Requested",session:"Actual"},Lt=({session:e})=>{const[r,s]=x.useState("desired"),a=ie(e.desired_capabilities),n=ie(e.session_capabilities),l=Object.entries(r==="desired"?a:n);return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] overflow-hidden",children:[t.jsx("header",{className:"flex items-center border-b border-[var(--border)]",children:["desired","session"].map(i=>t.jsx("button",{type:"button",onClick:()=>s(i),"aria-pressed":r===i,className:`flex-1 h-9 text-[11px] font-semibold transition-colors border-b-2 ${r===i?"border-[var(--color-accent)] text-[var(--text)]":"border-transparent text-[var(--text-muted)] hover:text-[var(--text)]"}`,children:fe[i]},i))}),l.length===0?t.jsxs("div",{className:"px-3 py-6 text-center text-xs text-[var(--text-dim)]",children:["No ",fe[r].toLowerCase()," capabilities recorded for this session."]}):t.jsx("dl",{className:"divide-y divide-[var(--border)]",children:l.map(([i,d])=>t.jsxs("div",{className:"grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)] items-baseline gap-3 px-3 py-2",children:[t.jsx("dt",{className:"text-xs text-[var(--text-muted)] break-all",children:i}),t.jsx("dd",{className:"font-mono text-xs text-[var(--text)] truncate text-right min-w-0",title:oe(d,500),children:oe(d)})]},i))})]})},Mt=({tabs:e,active:r,onChange:s,errorsOnly:a,onErrorsOnlyChange:n,showErrorsOnly:o=!0})=>{const l=e.filter(i=>!(i.hideWhenEmpty&&i.count===0));return t.jsxs("div",{className:"flex items-center justify-between gap-4 px-3 border-b border-[var(--border)] bg-[var(--surface)]",children:[t.jsx("div",{className:"flex items-center gap-1 overflow-x-auto",children:l.map(i=>{const d=i.key===r;return t.jsxs("button",{type:"button",onClick:()=>s(i.key),"aria-pressed":d,"data-empty":i.count===0?"true":"false",className:`relative inline-flex items-center gap-1.5 h-9 px-3 text-xs whitespace-nowrap transition-colors ${d?"text-[var(--text)]":i.count===0?"text-[var(--text-dim)] hover:text-[var(--text-muted)]":"text-[var(--text-muted)] hover:text-[var(--text)]"}`,children:[t.jsx("span",{children:i.label}),i.count>0&&t.jsx("span",{className:`text-[10px] tabular-nums ${d?"text-[var(--text-muted)]":"text-[var(--text-dim)]"}`,children:gt(i.count)}),d&&t.jsx("span",{className:"absolute left-2 right-2 bottom-0 h-[2px] bg-[var(--color-accent)] rounded-t"})]},i.key)})}),o&&t.jsxs("label",{className:"inline-flex items-center gap-2 text-[11px] text-[var(--text-muted)] cursor-pointer select-none whitespace-nowrap",children:[t.jsx("input",{type:"checkbox",checked:a,onChange:i=>n(i.target.checked),"aria-label":"Show only error rows"}),"Errors only"]})]})},Ot={key:"text-[var(--blue)]",string:"text-[var(--color-accent)]",number:"text-[var(--amber)]",boolean:"text-[var(--amber)]",null:"text-[var(--text-dim)] italic",punct:"text-[var(--text-muted)]",plain:"text-[var(--text)]"},Ft=({value:e,maxHeightPx:r})=>{const s=kt(e);if(!s)return t.jsx("span",{className:"text-[var(--text-dim)] text-xs",children:"(empty)"});let a;try{a=St(s)}catch{a=[{text:s,kind:"plain"}]}return t.jsx("pre",{className:"font-mono text-[11px] leading-relaxed bg-[var(--bg)] border border-[var(--border)] rounded-md p-3 overflow-x-auto whitespace-pre",style:r?{maxHeight:r,overflowY:"auto"}:void 0,children:a.map((n,o)=>t.jsx("span",{className:Ot[n.kind],children:n.text},o))})},Pt={green:"bg-[var(--color-success)]",red:"bg-[var(--red)]",amber:"bg-[var(--amber)]",blue:"bg-[var(--blue)]",neutral:"bg-[var(--text-dim)]"},Bt={green:"text-[var(--color-success)]",red:"text-[var(--red)]",amber:"text-[var(--amber)]",blue:"text-[var(--blue)]",neutral:"text-[var(--text-muted)]"},Ht=({log:e})=>{const[r,s]=x.useState(!1),{toast:a}=F(),n=I(e),o=wt(e),l=H(e),i=l?String(e.message):"",d=async m=>{m.stopPropagation();try{const b=l?i:e.body??e,f=typeof b=="string"?b:JSON.stringify(b,null,2);await navigator.clipboard.writeText(f),a("Log entry copied","success")}catch{a("Clipboard unavailable","error")}};return t.jsxs("div",{className:"border-b border-[var(--border)] last:border-b-0",children:[t.jsxs("button",{type:"button",onClick:()=>s(m=>!m),"aria-expanded":r,className:"group w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-[var(--surface-2)]/60 transition-colors",children:[t.jsx(V,{className:`h-3 w-3 text-[var(--text-dim)] transition-transform shrink-0 ${r?"rotate-90":""}`}),t.jsx("span",{className:"font-mono text-[11px] text-[var(--text-muted)] w-[64px] shrink-0",children:n}),t.jsx("span",{className:`h-1.5 w-1.5 rounded-full shrink-0 ${Pt[o.tone]}`}),o.label&&t.jsx("span",{className:`font-mono text-[11px] font-medium shrink-0 ${Bt[o.tone]}`,children:o.label}),t.jsxs("span",{className:"min-w-0 flex-1 flex items-baseline gap-2 truncate",children:[t.jsx("span",{className:l?"font-mono text-[11px] text-[var(--text)] truncate min-w-0":"text-[12px] text-[var(--text)] shrink-0",children:_t(e)}),ce(e)&&t.jsx("span",{className:"font-mono text-[11px] text-[var(--text-dim)] truncate min-w-0",children:ce(e)})]}),typeof e.duration=="number"&&t.jsx("span",{className:"text-[11px] tabular-nums text-[var(--text-muted)] shrink-0",children:O(e.duration)}),t.jsx("span",{role:"button",tabIndex:0,onClick:d,onKeyDown:m=>{(m.key==="Enter"||m.key===" ")&&d(m)},"aria-label":"Copy log entry",className:"opacity-0 group-hover:opacity-100 p-1 rounded text-[var(--text-dim)] hover:text-[var(--text)] transition-opacity shrink-0",children:t.jsx(z,{className:"h-3 w-3"})})]}),r&&t.jsx("div",{className:"px-3 pb-3 pl-9",children:l?t.jsx("pre",{className:"font-mono text-[11px] leading-relaxed text-[var(--text)] bg-[var(--bg)] border border-[var(--border)] rounded-md p-3 max-h-[400px] overflow-auto whitespace-pre-wrap break-words",children:i}):t.jsx(Ft,{value:e.body??e,maxHeightPx:400})})]})},It=(e,r)=>e?"bg-[var(--color-danger)]":r?"bg-[var(--color-warning)]":"bg-[var(--color-success)]",Ut=({commands:e})=>{const{spanMs:r,bars:s}=mt(e);return s.length===0?t.jsx("div",{className:"px-4 py-10 text-center text-xs text-[var(--text-dim)]",children:"No commands recorded for this session."}):t.jsxs("div",{className:"px-3 py-3",children:[t.jsxs("div",{className:"flex items-center justify-between pb-2 text-[11px] text-[var(--text-dim)] tabular-nums",children:[t.jsxs("span",{children:[s.length," command",s.length===1?"":"s"," over ",O(r)]}),t.jsxs("span",{className:"inline-flex items-center gap-3",children:[t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:"h-2 w-2 rounded-sm bg-[var(--color-danger)]"})," Failed"]}),t.jsxs("span",{className:"inline-flex items-center gap-1",children:[t.jsx("span",{className:"h-2 w-2 rounded-sm bg-[var(--color-warning)]"})," Healed"]})]})]}),t.jsx("ol",{className:"space-y-1",children:s.map((a,n)=>t.jsxs("li",{"data-testid":"timeline-bar","data-command":a.label,"data-failed":a.failed?"true":"false",className:"grid grid-cols-[128px_minmax(0,1fr)_64px] items-center gap-2",children:[t.jsx("span",{className:"truncate text-[11px] font-mono text-[var(--text-muted)]",title:a.label,children:a.label}),t.jsx("span",{className:"relative h-3 rounded-sm bg-[rgb(var(--rgb-fg)/0.05)]",children:t.jsx("span",{className:`absolute inset-y-0 rounded-sm ${It(a.failed,a.healed)}`,style:{left:`${a.startMs/r*100}%`,width:`max(2px, ${a.durationMs/r*100}%)`}})}),t.jsx("span",{className:"text-right text-[11px] tabular-nums text-[var(--text-muted)]",children:a.durationMs>0?O(a.durationMs):"—"})]},a.id??n))})]})},Vt=({commands:e})=>{const r=ut(e);return r.length===0?t.jsxs("div",{className:"px-4 py-10 text-center text-xs text-[var(--text-dim)]",children:["No screenshots were kept. A failed command keeps one unless"," ",t.jsx("code",{className:"font-mono",children:"xe:options.screenshotOnFailure"})," is false; set"," ",t.jsx("code",{className:"font-mono",children:"xe:options.screenshotOnEveryCommand"})," to keep one for every command."]}):t.jsx("ul",{className:"grid grid-cols-[repeat(auto-fill,minmax(140px,1fr))] gap-3 p-3",children:r.map((s,a)=>{const n=k.getAssetUrl(s.path),o=I({timestamp:s.at??null});return t.jsx("li",{children:t.jsxs("a",{href:n,target:"_blank",rel:"noreferrer",className:`block rounded-md border overflow-hidden hover:border-[var(--border-strong)] ${s.failed?"border-[var(--color-danger)]":"border-[var(--border)]"}`,children:[t.jsx("img",{src:n,alt:`${s.command} at ${o}`,loading:"lazy",className:"block w-full aspect-[9/16] object-contain bg-[rgb(var(--rgb-fg)/0.03)]"}),t.jsxs("span",{className:"flex items-center justify-between gap-2 px-2 py-1 text-[11px]",children:[t.jsx("span",{className:`truncate font-mono ${s.failed?"text-[var(--color-danger)]":"text-[var(--text)]"}`,children:s.command}),t.jsx("span",{className:"tabular-nums text-[var(--text-dim)] shrink-0",children:o})]})]})},s.id??a)})})},he=["commands","device","debug","profiling"],Jt=({sessionLogs:e,deviceLogs:r,debugLogs:s,profiling:a})=>{const[n,o]=x.useState("commands"),[l,i]=x.useState(!1),d=e,m=x.useMemo(()=>ke(d).screenshots,[d]),b=x.useMemo(()=>[{key:"commands",label:"Commands",count:e.length},{key:"timeline",label:"Timeline",count:e.length},{key:"screenshots",label:"Screenshots",count:m},{key:"device",label:"Device logs",count:r.length},{key:"debug",label:"Debug logs",count:s.length},{key:"profiling",label:"System profiling",count:a.length,hideWhenEmpty:!0}],[e.length,m,r.length,s.length,a.length]),f=x.useMemo(()=>{let v=[];return n==="commands"?v=e:n==="device"?v=r:n==="debug"?v=s:n==="profiling"&&(v=a),Ct(v,l)},[n,e,r,s,a,l]);return t.jsxs("section",{className:"rounded-lg border border-[var(--border)] bg-[var(--surface)] flex flex-col min-h-[280px] max-h-[640px]",children:[t.jsx(Mt,{tabs:b,active:n,onChange:o,errorsOnly:l,onErrorsOnlyChange:i,showErrorsOnly:he.includes(n)}),t.jsxs("div",{className:"flex-1 overflow-y-auto",children:[n==="timeline"&&t.jsx(Ut,{commands:d}),n==="screenshots"&&t.jsx(Vt,{commands:d}),he.includes(n)&&(f.length===0?t.jsx("div",{className:"px-4 py-10 text-center text-xs text-[var(--text-dim)]",children:l?"No error rows in this tab.":"No log entries in this tab."}):f.map((v,j)=>t.jsx(Ht,{log:v},v.id??j)))]})]})};function _e(e){if(!e)return"failed";switch(e){case"HTTPS_CLIENT_ERROR":case"HTTPS_SERVER_ERROR":case"OPEN_HTTPS_SERVER_ERROR":case"ON_CONNECT_ERROR":return"tls";case"PROXY_TO_SERVER_REQUEST_ERROR":return"net";default:return"failed"}}const Y="/xenon/api/interceptor";async function Wt(e){const r=await fetch(`${Y}/sessions/${encodeURIComponent(e)}/requests`);if(r.status===404){const a=await r.json().catch(()=>({}));return{active:!1,error:a==null?void 0:a.error}}return r.ok?{active:!0,requests:(await r.json()).requests||[]}:{active:!1,error:`request failed (${r.status})`}}async function zt(e,r){const s=await fetch(`${Y}/sessions/${encodeURIComponent(e)}/requests/${encodeURIComponent(r)}`);return s.ok?s.json():null}function Kt(e){return`${Y}/sessions/${encodeURIComponent(e)}/har`}function Gt(e){return e>=200&&e<300?"ready":e>=300&&e<400?"neutral":e>=400&&e<500?"busy":e>=500?"error":"neutral"}const pe=({headers:e})=>{const r=Object.entries(e||{});return r.length===0?t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:"No headers"}):t.jsx("div",{className:"space-y-0.5 font-mono text-xs",children:r.map(([s,a])=>t.jsxs("div",{className:"flex gap-2",children:[t.jsxs("span",{className:"text-[var(--text-dim)] shrink-0",children:[s,":"]}),t.jsx("span",{className:"break-all",children:a})]},s))})},ve=({body:e})=>{if(e==null||e==="")return t.jsx("div",{className:"text-xs text-[var(--text-dim)]",children:"No body"});let r=e;try{r=JSON.stringify(JSON.parse(e),null,2)}catch{}return t.jsx("pre",{className:"font-mono text-xs whitespace-pre-wrap break-all bg-[var(--surface-2,var(--surface))] rounded p-2 max-h-64 overflow-auto",children:r})},Xt=({request:e,onClose:r})=>{if(!e)return null;const s=t.jsxs("div",{className:"flex items-center gap-2 text-sm",children:[t.jsx(_,{tone:"accent",children:e.method}),e.failed?t.jsx(_,{tone:"error",children:_e(e.failureKind)}):t.jsx(_,{tone:Gt(e.resStatus),children:e.resStatus||"—"}),t.jsx("span",{className:"font-mono text-xs truncate",title:e.url,children:e.url})]});return t.jsx(st,{open:!!e,title:s,onClose:r,width:720,children:t.jsxs("div",{className:"space-y-4 text-xs",children:[e.failed&&t.jsxs("div",{className:"border border-[var(--red)] rounded p-2 bg-[rgb(var(--rgb-red-600)/0.08)]",children:[t.jsx("div",{className:"text-[var(--red)] text-[11px] mb-1",children:"Failure"}),t.jsx("div",{className:"text-xs",children:e.failureReason||"Request failed"}),e.failureKind&&t.jsx("div",{className:"font-mono text-[10px] text-[var(--text-dim)] mt-1",children:e.failureKind})]}),!e.failed&&t.jsxs("div",{className:"grid grid-cols-2 gap-3",children:[t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Duration"}),t.jsxs("div",{className:"font-mono",children:[e.durationMs," ms"]})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Flags"}),t.jsxs("div",{className:"flex gap-1",children:[e.mocked&&t.jsx(_,{tone:"accent",children:"mocked"}),e.modified&&t.jsx(_,{tone:"busy",children:"modified"}),!e.mocked&&!e.modified&&t.jsx("span",{className:"text-[var(--text-dim)]",children:"passthrough"})]})]})]}),e.commandHint&&t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Triggered by"}),t.jsx("div",{className:"font-mono",children:e.commandHint.commandName})]}),!e.failed&&t.jsxs(t.Fragment,{children:[t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Request Headers"}),t.jsx(pe,{headers:e.reqHeaders})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Request Body"}),t.jsx(ve,{body:e.reqBody})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Response Headers"}),t.jsx(pe,{headers:e.resHeaders})]}),t.jsxs("div",{children:[t.jsx("div",{className:"text-[var(--text-dim)] text-[11px] mb-1",children:"Response Body"}),t.jsx(ve,{body:e.resBody})]})]})]})})};function Yt(e){return e?new Date(e).toLocaleTimeString(void 0,{hour12:!1}):"—"}function qt(e){return e>=200&&e<300?"ready":e>=300&&e<400?"neutral":e>=400&&e<500?"busy":e>=500?"error":"neutral"}const Qt=({sessionId:e,sessionEnded:r})=>{const[s,a]=x.useState(null),[n,o]=x.useState(null),[l,i]=x.useState([]),[d,m]=x.useState(!0),[b,f]=x.useState(null),v=x.useRef(new Set),{on:j,isConnected:N}=$e();x.useEffect(()=>{let c=!0;return m(!0),Wt(e).then(u=>{if(!c)return;a(u.active),o(u.error||null);const p=u.requests||[];i(p),v.current=new Set(p.map(h=>h.id))}).finally(()=>{c&&m(!1)}),()=>{c=!1}},[e]),x.useEffect(()=>{if(r)return;const c=j("interceptor_request",h=>{!h||h.sessionId!==e||v.current.has(h.id)||(v.current.add(h.id),i(R=>[h,...R]),a(!0))}),u=j("interceptor_session_started",h=>{(h==null?void 0:h.sessionId)===e&&a(!0)}),p=j("interceptor_session_stopped",h=>{(h==null?void 0:h.sessionId)===e&&a(!1)});return()=>{c(),u(),p()}},[j,e,r,N]);const y=x.useMemo(()=>[...l].sort((c,u)=>u.ts-c.ts),[l]),w=async c=>{if(f(c),s){const u=await zt(e,c.id);u&&f(u)}},A=s&&l.length>0?t.jsxs("a",{href:Kt(e),className:"inline-flex items-center gap-1 text-xs text-[var(--text-dim)] hover:text-[var(--text)]",title:"Download HAR",children:[t.jsx(we,{className:"h-3 w-3"}),"HAR"]}):null,C=t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(at,{className:"h-3.5 w-3.5 text-[var(--text-dim)]"}),t.jsx("span",{children:"Network"}),s&&l.length>0&&t.jsxs("span",{className:"text-xs text-[var(--text-dim)]",children:["(",l.length,")"]})]});let g;return d?g=t.jsx("div",{className:"p-4 text-xs text-[var(--text-dim)]",children:"Loading…"}):s===!1&&r?g=t.jsx(E,{title:"No network capture",description:"Network interception was not enabled for this session."}):s===!1?g=t.jsx(E,{title:"Network interception disabled",description:"Enable interceptor.enabled in plugin config to capture requests for Android sessions."}):n?g=t.jsxs("div",{className:"p-4 text-xs text-[var(--red)]",children:["Failed to load requests: ",n]}):y.length===0?g=t.jsx(E,{title:"Waiting for requests",description:r?"No HTTP traffic was captured during this session.":"Captured requests will appear here as the device makes calls."}):g=t.jsx("div",{className:"overflow-auto max-h-96",children:t.jsxs(et,{children:[t.jsx(tt,{children:t.jsxs(se,{children:[t.jsx(T,{style:{width:80},children:"Time"}),t.jsx(T,{style:{width:70},children:"Method"}),t.jsx(T,{style:{width:70},children:"Status"}),t.jsx(T,{children:"Host"}),t.jsx(T,{children:"Path"}),t.jsx(T,{style:{width:80},children:"Duration"}),t.jsx(T,{style:{width:90},children:"Flags"})]})}),t.jsx(rt,{children:y.map(c=>t.jsxs(se,{onClick:()=>w(c),style:{cursor:"pointer"},title:c.failed?c.failureReason:void 0,children:[t.jsx($,{className:"font-mono text-[11px]",children:Yt(c.ts)}),t.jsx($,{children:t.jsx(_,{tone:"accent",children:c.method})}),t.jsx($,{children:c.failed?t.jsx(_,{tone:"error",children:_e(c.failureKind)}):t.jsx(_,{tone:qt(c.resStatus),children:c.resStatus||"—"})}),t.jsx($,{className:"font-mono text-[11px] truncate",title:c.host,children:c.host}),t.jsx($,{className:"font-mono text-[11px] truncate",title:c.path,children:c.failed?"—":c.path}),t.jsx($,{className:"font-mono text-[11px]",children:c.failed?"—":`${c.durationMs}ms`}),t.jsx($,{children:t.jsxs("div",{className:"flex gap-1",children:[c.mocked&&t.jsx(_,{tone:"accent",children:"mock"}),c.modified&&t.jsx(_,{tone:"busy",children:"mod"})]})})]},c.id))})]})}),t.jsxs(t.Fragment,{children:[t.jsx(ye,{header:C,action:A,padded:!1,children:g}),t.jsx(Xt,{request:b,onClose:()=>f(null)})]})},Nr=()=>{const{buildId:e="",sessionId:r=""}=Ae(),{toast:s}=F(),a=Ee(),n=Pe(),o=it(r||null),l=x.useMemo(()=>n.builds.find(f=>f.id===e)??null,[n.builds,e]),i=l?Be(l):"Build";if(x.useEffect(()=>{!o.loading&&o.notFound&&(s("Session not available — it may belong to a team you are not on.","info"),a("/builds",{replace:!0}))},[o.loading,o.notFound,a,s]),o.loading)return t.jsx("div",{className:"p-6 text-xs text-[var(--text-dim)]",children:"Loading session…"});if(o.error||!o.session)return t.jsxs("div",{className:"p-6 space-y-2",children:[t.jsx(ne,{buildId:e,buildName:l?i:null,sessionId:r}),t.jsx("div",{className:"mt-6 rounded-md border border-[var(--red)]/30 bg-[var(--surface)] p-4 text-xs text-[var(--red)]",children:o.error||"Session not found."})]});const d=o.session,m=o.sessionLogs,b=K(d.status).bucket==="failed";return t.jsxs("div",{className:"flex flex-col h-full min-h-0",children:[t.jsx(ne,{buildId:e,buildName:l?i:null,sessionId:d.id}),t.jsx(lt,{session:d}),t.jsx("div",{className:"flex-1 overflow-y-auto overflow-x-hidden",children:t.jsxs("div",{className:"px-4 py-4 space-y-4",children:[t.jsx(ft,{session:d,commands:m}),b&&t.jsx(bt,{session:d,buildName:i,buildId:e,commands:m,durationText:He(W(d))}),t.jsx(Rt,{commands:m}),t.jsx(At,{sessionId:d.id,running:d.status==="running",hasTrace:!!d.performance_trace}),t.jsxs("div",{className:"grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_340px] gap-4",children:[t.jsx("div",{className:"min-w-0",children:t.jsx(Jt,{sessionLogs:o.sessionLogs,deviceLogs:o.deviceLogs,debugLogs:o.debugLogs,profiling:o.profiling})}),t.jsxs("div",{className:"flex flex-col gap-4 min-w-0",children:[t.jsx(Et,{session:d}),t.jsx(Dt,{session:d,buildName:l?i:null}),t.jsx(Lt,{session:d})]})]}),t.jsx(Qt,{sessionId:d.id,sessionEnded:d.status!=="running"})]})})]})};export{Nr as SessionDetailPage,Nr as default};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{c as q,t as B,r,p as V,X as g,j as e,A as P,Q as X,C as G,e as O,V as U,W}from"./index-DI1fGIyj.js";/* empty css */import{S as j,A as K}from"./SettingCard-CWYmycoe.js";import{M as Q}from"./Modal-Cg9V5JYU.js";import{B as A}from"./button-fIW8owMI.js";import{P as J}from"./page-header--aTyJXq6.js";import{C as I}from"./circle-check-zJU80_EF.js";import{C as Y}from"./clock-BtUprDsZ.js";import{M as Z}from"./mouse-pointer-2-CXdKMXQ8.js";import{C as ee}from"./check-BdTh1Sq7.js";import{A as ae}from"./activity-Bdf_uIWl.js";import{C as se}from"./calendar-WNjmrF-a.js";import{A as te}from"./arrow-up-right-eFG5KTFS.js";import"./index-HDQVe1gi.js";import"./utils-CDN07tui.js";/**
|
|
2
|
+
* @license lucide-react v0.555.0 - ISC
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the ISC license.
|
|
5
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/const ne=[["path",{d:"M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z",key:"1oefj6"}],["path",{d:"M14 2v5a1 1 0 0 0 1 1h5",key:"wfsgrz"}],["path",{d:"M10 9H8",key:"b1mrlr"}],["path",{d:"M16 13H8",key:"t4e002"}],["path",{d:"M16 17H8",key:"z1uh3a"}]],le=q("file-text",ne),m={healthCheckIntervalMs:3e4,healthCheckSchedule:"",enableSelfHealing:!0},S=5e3,ie=[{label:"Battery saver (2 AM)",value:"0 2 * * *"},{label:"Standard (hourly)",value:"0 * * * *"},{label:"Operational coverage (30m)",value:"*/30 * * * *"},{label:"High performance (10m)",value:"*/10 * * * *"}],N=5;function re(t){const s=Date.parse(t);if(!Number.isFinite(s))return"";const n=Math.max(0,Date.now()-s),i=Math.floor(n/1e3);if(i<60)return`${i}s ago`;const h=Math.floor(i/60);if(h<60)return`${h}m ago`;const d=Math.floor(h/60);return d<24?`${d}h ago`:`${Math.floor(d/24)}d ago`}function E(t){var d,u;const s=(d=t.originalSelector)==null?void 0:d.trim(),n=(u=t.healedSelector)==null?void 0:u.trim(),i=t.tier&&t.commandName?`${t.commandName}: `:"";if(s&&n&&s!==n)return`${i}${s} → ${n}`;if(n)return`${i}Recovered locator ${n}`;if(s)return`${i}Recovered locator ${s}`;const h=typeof t.confidence=="number"?` (${Math.round(t.confidence*100)}%)`:"";return`${i||""}Self-healed${h}`}function ce(t){return t.tier?t.tier:t.commandName?t.commandName:t.deviceName?t.deviceName:"Self-heal"}const oe=(t,s)=>t.healthCheckIntervalMs===s.healthCheckIntervalMs&&t.healthCheckSchedule===s.healthCheckSchedule&&t.enableSelfHealing===s.enableSelfHealing,ke=()=>{const{toast:t}=B(),[s,n]=r.useState(m),[i,h]=r.useState(m),[d,u]=r.useState(!0),[y,C]=r.useState(!1),[H,f]=r.useState(!1),[b,k]=r.useState([]),[w,z]=r.useState(!1),{on:M}=V();r.useEffect(()=>{$()},[]),r.useEffect(()=>{let a=!1;return g.getRecentHealingEvents(N).then(l=>{if(a||!l)return;const o=Array.isArray(l.events)?l.events.slice(0,N):[];k(o)}).catch(()=>{}).finally(()=>{a||z(!0)}),()=>{a=!0}},[]),r.useEffect(()=>{const a=M("healing_event",l=>{const o=l;!o||!o.id||k(x=>x.some(F=>F.id===o.id)?x:[o,...x].slice(0,N))});return()=>{a&&a()}},[M]);const $=async()=>{u(!0);try{const a=await g.getGlobalConfig(),l={healthCheckIntervalMs:a.healthCheckIntervalMs||3e4,healthCheckSchedule:a.healthCheckSchedule||"",enableSelfHealing:a.enableSelfHealing!==void 0?a.enableSelfHealing:!0};n(l),h(l)}catch(a){console.error("Failed to load settings",a),t("Failed to access infrastructure parameters.","error")}finally{u(!1)}},v=!oe(s,i),c=Number.isNaN(s.healthCheckIntervalMs)||s.healthCheckIntervalMs<S?`Below minimum safe value of ${S}ms.`:null,R=v&&!c,p={interval:s.healthCheckIntervalMs!==i.healthCheckIntervalMs,schedule:s.healthCheckSchedule!==i.healthCheckSchedule,healing:s.enableSelfHealing!==i.enableSelfHealing},_=async a=>{const l=a??s;if(!a&&c){t(c,"error");return}C(!0);try{await g.updateGlobalConfig(l),h(l),n(l),t("Infrastructure parameters synchronized across fleet.","success")}catch(o){console.error("Failed to save settings",o),W(t,o)}finally{C(!1)}},L=async()=>{n(m),await _(m);try{await g.resetMetrics()}catch(a){console.error("Failed to reset metrics",a)}},D=()=>{n(i)},T=s.healthCheckSchedule!=="";return d?e.jsxs("div",{className:"settings-loading",children:[e.jsx(P,{className:"animate-spin",size:32}),e.jsx("span",{children:"Synchronizing Global Infrastructure..."})]}):e.jsxs("div",{className:"settings-container",children:[e.jsx(J,{icon:X,title:"Settings",subtitle:"Manage core farm parameters, heartbeat frequency, and maintenance orchestrations across the global registry."}),e.jsx("div",{className:"settings-content",children:e.jsxs("div",{className:"settings-grid settings-grid--three",children:[e.jsxs(j,{icon:e.jsx(Y,{size:16}),title:e.jsxs("span",{className:"card-title-row",children:["Idle health frequency",p.interval&&e.jsx("span",{className:"modified-dot","aria-label":"Modified"})]}),description:"Frequency of passive health pings when the system is in idle state.",hint:"Minimum safe value: 5000ms. Note: This frequency is overridden when a schedule is active.",children:[e.jsx("div",{className:"card-spacer"}),e.jsxs("div",{className:`input-group ${c?"has-error":""}`,children:[e.jsx("input",{type:"number",value:s.healthCheckIntervalMs,onChange:a=>n({...s,healthCheckIntervalMs:parseInt(a.target.value,10)}),min:S,step:5e3,"aria-invalid":!!c,"aria-describedby":c?"interval-error":"interval-status"}),e.jsx("span",{className:"code-font",children:"MS"})]}),c?e.jsxs("div",{id:"interval-error",className:"input-status input-status--error",role:"alert",children:[e.jsx(G,{size:12}),e.jsx("span",{children:c})]}):e.jsxs("div",{id:"interval-status",className:"input-status input-status--ok",children:[e.jsx(I,{size:12}),e.jsx("span",{children:"Valid frequency"})]})]}),e.jsxs(j,{icon:e.jsx(se,{size:16}),title:e.jsxs("span",{className:"card-title-row",children:["Deep diagnostic schedule",p.schedule&&e.jsx("span",{className:"modified-dot","aria-label":"Modified"})]}),titleExtra:T?e.jsxs("span",{className:"active-pill",children:[e.jsx(ae,{size:11}),e.jsx("span",{children:"Active"})]}):null,description:"Execute intensive reliability bursts (WDA restarts, Cache purges) using standardized Cron syntax.",children:[e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{type:"text",placeholder:"e.g. 0 * * * * (hourly, at minute 0)",value:s.healthCheckSchedule,onChange:a=>n({...s,healthCheckSchedule:a.target.value})})}),e.jsxs("div",{className:"cron-presets",children:[e.jsxs("div",{className:"presets-label",children:[e.jsx(Z,{size:12}),e.jsx("span",{children:"Intent-based presets"})]}),e.jsx("div",{className:"presets-grid presets-grid--stacked",children:ie.map(a=>{const l=s.healthCheckSchedule===a.value;return e.jsxs("button",{type:"button",className:`preset-chip ${l?"active":""}`,onClick:()=>n({...s,healthCheckSchedule:a.value}),"aria-pressed":l,children:[l&&e.jsx(ee,{size:11,className:"preset-chip__check"}),e.jsx("span",{children:a.label})]},a.label)})})]})]}),e.jsxs(j,{icon:e.jsx(U,{size:16}),title:e.jsxs("span",{className:"card-title-row",children:["AI self-healing",p.healing&&e.jsx("span",{className:"modified-dot","aria-label":"Modified"})]}),titleExtra:e.jsxs("label",{className:"switch switch--header",children:[e.jsx("input",{type:"checkbox",checked:s.enableSelfHealing,onChange:a=>n({...s,enableSelfHealing:a.target.checked}),"aria-label":"Toggle AI self-healing"}),e.jsx("span",{className:"slider round"})]}),children:[e.jsx("div",{className:`healing-state ${s.enableSelfHealing?"is-on":"is-off"}`,children:s.enableSelfHealing?"Enabled":"Disabled"}),e.jsx("p",{className:"setting-card-description",children:"Automatically intercept and recover from failing locators using Xenon's 6-tier strategy: etalon recovery, native retry, fuzzy XML, OCR, visual AI, and LLM — before failing a test."}),s.enableSelfHealing&&e.jsxs("div",{className:"healing-events",children:[e.jsxs("div",{className:"healing-events__header",children:[e.jsx(le,{size:11}),e.jsx("span",{children:"Recent healing events"}),e.jsxs(O,{to:"/selector-health",className:"healing-events__link",children:["Selector health ",e.jsx(te,{size:10})]})]}),b.length>0?e.jsx("ul",{className:"healing-events__list",children:b.map(a=>e.jsxs("li",{className:"healing-event",children:[e.jsx(I,{size:14,className:"healing-event__icon"}),e.jsxs("div",{className:"healing-event__body",children:[e.jsxs("div",{className:"healing-event__row",children:[e.jsx("span",{className:"healing-event__kind",children:ce(a)}),e.jsx("span",{className:"healing-event__when",children:re(a.createdAt)})]}),e.jsx("div",{className:"healing-event__message",title:E(a),children:E(a)})]})]},a.id))}):e.jsx("div",{className:"healing-events__empty",children:w?"No healing events recorded yet — the dashboard will populate as locators are recovered.":"Loading recent events…"})]})]})]})}),(v||y)&&e.jsx(K,{onSave:()=>_(),onDiscard:D,onRestoreDefaults:()=>f(!0),isSaving:y,isValidating:!!c,isDirty:v,saveLabel:R?"Save Configuration":"Resolve errors to save"}),e.jsxs(Q,{open:H,onClose:()=>f(!1),title:"Restore default settings?",footer:e.jsxs(e.Fragment,{children:[e.jsx(A,{variant:"secondary",onClick:()=>f(!1),children:"Cancel"}),e.jsx(A,{variant:"danger",onClick:async()=>{f(!1),await L()},children:"Restore defaults"})]}),children:[e.jsx("p",{className:"text-sm text-[var(--text-muted)]",children:"This saves immediately and can't be undone:"}),e.jsxs("ul",{className:"mt-2 list-disc pl-5 text-sm text-[var(--text)] space-y-1",children:[e.jsxs("li",{children:["Idle health frequency goes back to ",m.healthCheckIntervalMs.toLocaleString()," ms"]}),e.jsx("li",{children:"The diagnostic schedule is cleared"}),e.jsx("li",{children:"AI self-healing is turned on"}),e.jsx("li",{children:"Every device's healed-selector count is reset to zero"})]})]})]})};export{ke as Settings};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as t}from"./index-
|
|
1
|
+
import{j as t}from"./index-DI1fGIyj.js";const d={neutral:"text-[var(--text-muted)]",good:"text-[var(--color-success)]",bad:"text-[var(--color-danger)]"},o=({label:e,value:r,valueCls:a="text-[var(--text)]",note:s,noteTone:x="neutral"})=>t.jsxs("div",{className:"min-w-0 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-3",children:[t.jsx("div",{className:"text-xs text-[var(--text-muted)]",children:e}),t.jsx("div",{className:`mt-1 text-2xl font-semibold tabular-nums leading-tight truncate ${a}`,children:r}),t.jsx("div",{className:`mt-0.5 text-[11px] truncate ${d[x]}`,children:s??" "})]});export{o as S};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as A,r as n,j as e,A as I,Y as f,q as $}from"./index-CHOr4JCs.js";/* empty css */import{a as F}from"./time-D-gjmE1o.js";import{T as E,a as M,b as T,c as u,d as U,e as p}from"./Table-DlEuFgv-.js";import{F as K}from"./FieldGroup-DqMf7Rf3.js";import{P as R}from"./page-header-Cm1XL49f.js";import{M as Q}from"./Modal-DfawTxih.js";import{B as D}from"./button-BxnHCCsX.js";import{I as V}from"./input-CJ8z3IO5.js";import{S as P}from"./select-CZuCnold.js";import{l as W}from"./users-BpYnpOl2.js";import{P as N}from"./plus-CIIvR_6Z.js";import{A as X}from"./arrow-left-CLQWAiCY.js";import{T as L}from"./trash-2-CcSsv4fS.js";import"./index-BiL0Enl_.js";import"./utils-CDN07tui.js";const B={SUPER_ADMIN:"Super admin",ADMIN:"Admin",MEMBER:"Member"};async function j(i,r){const t=await fetch(i,{credentials:"include",...r});if(!t.ok){const c=await t.json().catch(()=>({}));throw Object.assign(new Error(c.error||`HTTP ${t.status}`),{status:t.status})}return t.json()}const ve=()=>{const{toast:i}=A(),[r,t]=n.useState([]),[c,m]=n.useState(!0),[l,h]=n.useState(!1),[x,d]=n.useState(!1),[_,C]=n.useState(null),v=async()=>{m(!0);try{const a=await j("/xenon/api/teams");t(a),h(!1)}catch(a){a.status===403?h(!0):i(`Failed to load teams: ${a.message}`,"error")}finally{m(!1)}};if(n.useEffect(()=>{v()},[]),c)return e.jsxs("div",{className:"settings-loading",children:[e.jsx(I,{className:"animate-spin",size:32}),e.jsx("span",{children:"Loading teams…"})]});if(l)return e.jsx("div",{className:"settings-container",children:e.jsx(R,{icon:f,title:"Teams",subtitle:e.jsxs(e.Fragment,{children:["Your key lacks the ",e.jsx("code",{children:"admin"})," scope. Ask an administrator to promote your key before you can manage teams."]})})});if(_)return e.jsx(se,{team:_,onBack:()=>{C(null),v()}});const S=r.reduce((a,o)=>a+o.deviceCount,0),y=r.reduce((a,o)=>a+o.memberCount,0);return e.jsxs("div",{className:"settings-container",children:[e.jsx(R,{icon:f,title:"Teams",subtitle:"Group devices and members by team. Devices with no team assignment stay in the shared pool — visible to every authenticated user.",action:r.length>0?e.jsxs("button",{type:"button",className:"page-header-action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"New team"})]}):void 0}),e.jsxs("div",{className:"settings-content",children:[r.length>0&&e.jsxs("div",{className:"stat-strip",children:[e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value stat-strip__value--accent",children:r.length}),e.jsx("div",{className:"stat-strip__label",children:"Teams"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:y}),e.jsx("div",{className:"stat-strip__label",children:"Members"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:S}),e.jsx("div",{className:"stat-strip__label",children:"Devices assigned"})]})]}),r.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(f,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No teams yet"}),e.jsx("p",{className:"empty-state__copy",children:"Every device is in the shared pool until you create a team and assign it. Teams let you scope an API key to a subset of devices — useful for separating CI runners, staging fleets, or per-team device pools."}),e.jsxs("button",{type:"button",className:"page-header-action empty-state__action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"Create your first team"})]})]}):e.jsx("div",{className:"team-grid",children:r.map(a=>e.jsxs("button",{type:"button",className:"team-card",onClick:()=>C(a),children:[e.jsxs("div",{className:"team-card__header",children:[e.jsx("div",{className:"team-card__icon",children:e.jsx(f,{size:16})}),e.jsxs("div",{className:"team-card__title",children:[e.jsx("div",{className:"team-card__name",children:a.name}),e.jsx("div",{className:"team-card__id",children:a.id.slice(0,8)})]}),e.jsx("span",{className:"team-card__delete",role:"button",tabIndex:0,onClick:o=>o.stopPropagation(),children:e.jsx(ee,{team:a,onDeleted:v})})]}),e.jsxs("div",{className:"team-card__metrics",children:[e.jsxs("div",{className:"team-card__metric",children:[e.jsx($,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.deviceCount}),e.jsx("span",{className:"team-card__metric-label",children:"devices"})]}),e.jsxs("div",{className:"team-card__metric",children:[e.jsx(f,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.memberCount}),e.jsx("span",{className:"team-card__metric-label",children:"members"})]})]}),e.jsxs("div",{className:"team-card__footer",children:["Created ",F(a.createdAt)]})]},a.id))})]}),x&&e.jsx(Z,{onClose:()=>d(!1),onCreated:()=>{d(!1),v()}})]})},Z=({onClose:i,onCreated:r})=>{const{toast:t}=A(),[c,m]=n.useState(""),[l,h]=n.useState(!1),x=async()=>{if(!c.trim())return t("Name required","error");h(!0);try{await j("/xenon/api/teams",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:c.trim()})}),t("Team created","success"),r()}catch(d){t(d.message,"error")}finally{h(!1)}};return e.jsx(Q,{open:!0,onClose:i,title:"New team",footer:e.jsxs(e.Fragment,{children:[e.jsx(D,{variant:"secondary",onClick:i,disabled:l,children:"Cancel"}),e.jsxs(D,{variant:"primary",onClick:x,disabled:l,children:[l?e.jsx(I,{className:"animate-spin",size:18}):e.jsx(N,{size:18}),l?"Creating…":"Create"]})]}),children:e.jsx("div",{style:{display:"flex",flexDirection:"column"},children:e.jsx(K,{label:"Name",htmlFor:"team-name",children:e.jsx(V,{id:"team-name",type:"text",value:c,onChange:d=>m(d.target.value),placeholder:"android-team, qa-ios, …",className:"w-full",autoFocus:!0})})})})},ee=({team:i,onDeleted:r})=>{const{toast:t}=A(),[c,m]=n.useState(!1),l=async h=>{if(h.stopPropagation(),!!window.confirm(`Delete team "${i.name}"? It must have no devices, members or apps.`)){m(!0);try{await j(`/xenon/api/teams/${i.id}`,{method:"DELETE"}),t("Team deleted","success"),r()}catch(x){t(x.message,"error")}finally{m(!1)}}};return e.jsxs("button",{className:"reset-btn",disabled:c,onClick:l,style:{color:"var(--status-error-fg)"},children:[e.jsx(L,{size:14}),c?"Deleting…":"Delete"]})},se=({team:i,onBack:r})=>{const{toast:t}=A(),[c,m]=n.useState([]),[l,h]=n.useState([]),[x,d]=n.useState([]),[_,C]=n.useState([]),[v,S]=n.useState(!0),[y,a]=n.useState(""),[o,k]=n.useState(""),b=async()=>{S(!0);try{const[s,g,Y]=await Promise.all([j(`/xenon/api/teams/${i.id}/members`),W(),j("/xenon/api/device")]);m(s),h(g);const z=Y;d(z.filter(w=>w.teamId===i.id)),C(z.filter(w=>!w.teamId))}catch(s){t(`Failed to load team detail: ${s.message}`,"error")}finally{S(!1)}};n.useEffect(()=>{b()},[i.id]);const O=async()=>{if(y)try{await j(`/xenon/api/teams/${i.id}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userId:y})}),t("Member added","success"),a(""),b()}catch(s){t(s.message,"error")}},H=async s=>{if(window.confirm("Remove this member from the team?"))try{await j(`/xenon/api/teams/${i.id}/members/${encodeURIComponent(s)}`,{method:"DELETE"}),t("Member removed","success"),b()}catch(g){t(g.message,"error")}},J=async()=>{if(o)try{await j(`/xenon/api/device/${encodeURIComponent(o)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:i.id})}),t("Device assigned","success"),k(""),b()}catch(s){t(s.message,"error")}},q=async s=>{if(window.confirm("Return this device to the shared pool?"))try{await j(`/xenon/api/device/${encodeURIComponent(s)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:null})}),t("Device unassigned","success"),b()}catch(g){t(g.message,"error")}},G=l.filter(s=>!c.some(g=>g.userId===s.id));return e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("button",{onClick:r,className:"reset-btn",style:{padding:"4px 8px"},"aria-label":"Back",children:e.jsx(X,{size:18})}),e.jsxs("div",{className:"settings-title-group",children:[e.jsx(f,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:i.name})]})]}),e.jsx("p",{className:"settings-subtitle",children:'Manage members and devices assigned to this team. To add a device, go to Devices and use "Assign to team."'})]}),v?e.jsx("div",{className:"settings-loading",children:e.jsx(I,{className:"animate-spin",size:32})}):e.jsxs("div",{className:"settings-content",style:{display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Members (",c.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:y,onChange:s=>a(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a user to add…"}),G.map(s=>e.jsxs("option",{value:s.id,children:[s.name," (",s.email,") — ",B[s.role]]},s.id))]}),e.jsxs(D,{variant:"primary",onClick:O,disabled:!y,children:[e.jsx(N,{size:16})," Add"]})]}),c.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No members yet. Add a user above."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Email"}),e.jsx(u,{children:"Role"}),e.jsx(u,{children:"Added"}),e.jsx(u,{style:{textAlign:"right"},children:"Action"})]})}),e.jsx(U,{children:c.map(s=>e.jsxs(T,{children:[e.jsx(p,{children:s.name}),e.jsx(p,{children:e.jsx("code",{style:{fontSize:"11px",color:"var(--text-muted)"},children:s.email})}),e.jsx(p,{children:B[s.role]}),e.jsx(p,{children:F(s.addedAt)}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>H(s.userId),style:{color:"var(--status-error-fg)"},"aria-label":"Remove member",title:"Remove member",children:e.jsx(L,{size:14})})})]},s.userId))})]})]})]}),e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Devices (",x.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:o,onChange:s=>k(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a shared-pool device to assign…"}),_.map(s=>e.jsxs("option",{value:s.udid,children:[s.name," (",s.platform,") — ",s.udid.slice(0,12),"…"]},s.udid))]}),e.jsxs(D,{variant:"primary",onClick:J,disabled:!o,children:[e.jsx(N,{size:16})," Assign"]})]}),x.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No devices assigned. Use the Devices page to move devices into this team."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Platform"}),e.jsx(u,{children:"UDID"}),e.jsx(u,{style:{textAlign:"right"},children:"Actions"})]})}),e.jsx(U,{children:x.map(s=>e.jsxs(T,{children:[e.jsxs(p,{children:[e.jsx($,{size:14,style:{marginRight:6,verticalAlign:"middle"}}),s.name]}),e.jsx(p,{children:s.platform}),e.jsx(p,{children:e.jsxs("code",{style:{fontSize:"11px"},children:[s.udid.slice(0,14),"…"]})}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>q(s.udid),children:"Return to shared pool"})})]},s.udid))})]})]})]})]})]})};export{ve as Teams};
|
|
1
|
+
import{t as A,r as n,j as e,A as I,Z as f,q as $}from"./index-DI1fGIyj.js";/* empty css */import{a as F}from"./time-D-gjmE1o.js";import{T as E,a as M,b as T,c as u,d as U,e as p}from"./Table-io9fwa98.js";import{F as Z}from"./FieldGroup-Bvn08svv.js";import{P as R}from"./page-header--aTyJXq6.js";import{M as K}from"./Modal-Cg9V5JYU.js";import{B as D}from"./button-fIW8owMI.js";import{I as Q}from"./input-BjDRKCea.js";import{S as P}from"./select-DvCSv25z.js";import{l as V}from"./users-DMF9Mk5H.js";import{P as N}from"./plus-CsIk10Cq.js";import{A as W}from"./arrow-left-B27ckB31.js";import{T as L}from"./trash-2-CTZpPSdd.js";import"./index-HDQVe1gi.js";import"./utils-CDN07tui.js";const B={SUPER_ADMIN:"Super admin",ADMIN:"Admin",MEMBER:"Member"};async function j(i,r){const t=await fetch(i,{credentials:"include",...r});if(!t.ok){const c=await t.json().catch(()=>({}));throw Object.assign(new Error(c.error||`HTTP ${t.status}`),{status:t.status})}return t.json()}const ve=()=>{const{toast:i}=A(),[r,t]=n.useState([]),[c,m]=n.useState(!0),[l,h]=n.useState(!1),[x,d]=n.useState(!1),[_,C]=n.useState(null),v=async()=>{m(!0);try{const a=await j("/xenon/api/teams");t(a),h(!1)}catch(a){a.status===403?h(!0):i(`Failed to load teams: ${a.message}`,"error")}finally{m(!1)}};if(n.useEffect(()=>{v()},[]),c)return e.jsxs("div",{className:"settings-loading",children:[e.jsx(I,{className:"animate-spin",size:32}),e.jsx("span",{children:"Loading teams…"})]});if(l)return e.jsx("div",{className:"settings-container",children:e.jsx(R,{icon:f,title:"Teams",subtitle:e.jsxs(e.Fragment,{children:["Your key lacks the ",e.jsx("code",{children:"admin"})," scope. Ask an administrator to promote your key before you can manage teams."]})})});if(_)return e.jsx(se,{team:_,onBack:()=>{C(null),v()}});const S=r.reduce((a,o)=>a+o.deviceCount,0),y=r.reduce((a,o)=>a+o.memberCount,0);return e.jsxs("div",{className:"settings-container",children:[e.jsx(R,{icon:f,title:"Teams",subtitle:"Group devices and members by team. Devices with no team assignment stay in the shared pool — visible to every authenticated user.",action:r.length>0?e.jsxs("button",{type:"button",className:"page-header-action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"New team"})]}):void 0}),e.jsxs("div",{className:"settings-content",children:[r.length>0&&e.jsxs("div",{className:"stat-strip",children:[e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value stat-strip__value--accent",children:r.length}),e.jsx("div",{className:"stat-strip__label",children:"Teams"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:y}),e.jsx("div",{className:"stat-strip__label",children:"Members"})]}),e.jsxs("div",{className:"stat-strip__cell",children:[e.jsx("div",{className:"stat-strip__value",children:S}),e.jsx("div",{className:"stat-strip__label",children:"Devices assigned"})]})]}),r.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(f,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No teams yet"}),e.jsx("p",{className:"empty-state__copy",children:"Every device is in the shared pool until you create a team and assign it. Teams let you scope an API key to a subset of devices — useful for separating CI runners, staging fleets, or per-team device pools."}),e.jsxs("button",{type:"button",className:"page-header-action empty-state__action",onClick:()=>d(!0),children:[e.jsx(N,{size:16}),e.jsx("span",{children:"Create your first team"})]})]}):e.jsx("div",{className:"team-grid",children:r.map(a=>e.jsxs("button",{type:"button",className:"team-card",onClick:()=>C(a),children:[e.jsxs("div",{className:"team-card__header",children:[e.jsx("div",{className:"team-card__icon",children:e.jsx(f,{size:16})}),e.jsxs("div",{className:"team-card__title",children:[e.jsx("div",{className:"team-card__name",children:a.name}),e.jsx("div",{className:"team-card__id",children:a.id.slice(0,8)})]}),e.jsx("span",{className:"team-card__delete",role:"button",tabIndex:0,onClick:o=>o.stopPropagation(),children:e.jsx(ee,{team:a,onDeleted:v})})]}),e.jsxs("div",{className:"team-card__metrics",children:[e.jsxs("div",{className:"team-card__metric",children:[e.jsx($,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.deviceCount}),e.jsx("span",{className:"team-card__metric-label",children:"devices"})]}),e.jsxs("div",{className:"team-card__metric",children:[e.jsx(f,{size:13}),e.jsx("span",{className:"team-card__metric-value",children:a.memberCount}),e.jsx("span",{className:"team-card__metric-label",children:"members"})]})]}),e.jsxs("div",{className:"team-card__footer",children:["Created ",F(a.createdAt)]})]},a.id))})]}),x&&e.jsx(X,{onClose:()=>d(!1),onCreated:()=>{d(!1),v()}})]})},X=({onClose:i,onCreated:r})=>{const{toast:t}=A(),[c,m]=n.useState(""),[l,h]=n.useState(!1),x=async()=>{if(!c.trim())return t("Name required","error");h(!0);try{await j("/xenon/api/teams",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:c.trim()})}),t("Team created","success"),r()}catch(d){t(d.message,"error")}finally{h(!1)}};return e.jsx(K,{open:!0,onClose:i,title:"New team",footer:e.jsxs(e.Fragment,{children:[e.jsx(D,{variant:"secondary",onClick:i,disabled:l,children:"Cancel"}),e.jsxs(D,{variant:"primary",onClick:x,disabled:l,children:[l?e.jsx(I,{className:"animate-spin",size:18}):e.jsx(N,{size:18}),l?"Creating…":"Create"]})]}),children:e.jsx("div",{style:{display:"flex",flexDirection:"column"},children:e.jsx(Z,{label:"Name",htmlFor:"team-name",children:e.jsx(Q,{id:"team-name",type:"text",value:c,onChange:d=>m(d.target.value),placeholder:"android-team, qa-ios, …",className:"w-full",autoFocus:!0})})})})},ee=({team:i,onDeleted:r})=>{const{toast:t}=A(),[c,m]=n.useState(!1),l=async h=>{if(h.stopPropagation(),!!window.confirm(`Delete team "${i.name}"? It must have no devices, members or apps.`)){m(!0);try{await j(`/xenon/api/teams/${i.id}`,{method:"DELETE"}),t("Team deleted","success"),r()}catch(x){t(x.message,"error")}finally{m(!1)}}};return e.jsxs("button",{className:"reset-btn",disabled:c,onClick:l,style:{color:"var(--status-error-fg)"},children:[e.jsx(L,{size:14}),c?"Deleting…":"Delete"]})},se=({team:i,onBack:r})=>{const{toast:t}=A(),[c,m]=n.useState([]),[l,h]=n.useState([]),[x,d]=n.useState([]),[_,C]=n.useState([]),[v,S]=n.useState(!0),[y,a]=n.useState(""),[o,k]=n.useState(""),b=async()=>{S(!0);try{const[s,g,Y]=await Promise.all([j(`/xenon/api/teams/${i.id}/members`),V(),j("/xenon/api/device")]);m(s),h(g);const z=Y;d(z.filter(w=>w.teamId===i.id)),C(z.filter(w=>!w.teamId))}catch(s){t(`Failed to load team detail: ${s.message}`,"error")}finally{S(!1)}};n.useEffect(()=>{b()},[i.id]);const O=async()=>{if(y)try{await j(`/xenon/api/teams/${i.id}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({userId:y})}),t("Member added","success"),a(""),b()}catch(s){t(s.message,"error")}},H=async s=>{if(window.confirm("Remove this member from the team?"))try{await j(`/xenon/api/teams/${i.id}/members/${encodeURIComponent(s)}`,{method:"DELETE"}),t("Member removed","success"),b()}catch(g){t(g.message,"error")}},J=async()=>{if(o)try{await j(`/xenon/api/device/${encodeURIComponent(o)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:i.id})}),t("Device assigned","success"),k(""),b()}catch(s){t(s.message,"error")}},q=async s=>{if(window.confirm("Return this device to the shared pool?"))try{await j(`/xenon/api/device/${encodeURIComponent(s)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:null})}),t("Device unassigned","success"),b()}catch(g){t(g.message,"error")}},G=l.filter(s=>!c.some(g=>g.userId===s.id));return e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("button",{onClick:r,className:"reset-btn",style:{padding:"4px 8px"},"aria-label":"Back",children:e.jsx(W,{size:18})}),e.jsxs("div",{className:"settings-title-group",children:[e.jsx(f,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:i.name})]})]}),e.jsx("p",{className:"settings-subtitle",children:'Manage members and devices assigned to this team. To add a device, go to Devices and use "Assign to team."'})]}),v?e.jsx("div",{className:"settings-loading",children:e.jsx(I,{className:"animate-spin",size:32})}):e.jsxs("div",{className:"settings-content",style:{display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Members (",c.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:y,onChange:s=>a(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a user to add…"}),G.map(s=>e.jsxs("option",{value:s.id,children:[s.name," (",s.email,") — ",B[s.role]]},s.id))]}),e.jsxs(D,{variant:"primary",onClick:O,disabled:!y,children:[e.jsx(N,{size:16})," Add"]})]}),c.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No members yet. Add a user above."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Email"}),e.jsx(u,{children:"Role"}),e.jsx(u,{children:"Added"}),e.jsx(u,{style:{textAlign:"right"},children:"Action"})]})}),e.jsx(U,{children:c.map(s=>e.jsxs(T,{children:[e.jsx(p,{children:s.name}),e.jsx(p,{children:e.jsx("code",{style:{fontSize:"11px",color:"var(--text-muted)"},children:s.email})}),e.jsx(p,{children:B[s.role]}),e.jsx(p,{children:F(s.addedAt)}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>H(s.userId),style:{color:"var(--status-error-fg)"},"aria-label":"Remove member",title:"Remove member",children:e.jsx(L,{size:14})})})]},s.userId))})]})]})]}),e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Devices (",x.length,")"]}),e.jsxs("div",{className:"surface-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs(P,{value:o,onChange:s=>k(s.target.value),className:"flex-1",children:[e.jsx("option",{value:"",children:"Select a shared-pool device to assign…"}),_.map(s=>e.jsxs("option",{value:s.udid,children:[s.name," (",s.platform,") — ",s.udid.slice(0,12),"…"]},s.udid))]}),e.jsxs(D,{variant:"primary",onClick:J,disabled:!o,children:[e.jsx(N,{size:16})," Assign"]})]}),x.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No devices assigned. Use the Devices page to move devices into this team."}):e.jsxs(E,{children:[e.jsx(M,{children:e.jsxs(T,{children:[e.jsx(u,{children:"Name"}),e.jsx(u,{children:"Platform"}),e.jsx(u,{children:"UDID"}),e.jsx(u,{style:{textAlign:"right"},children:"Actions"})]})}),e.jsx(U,{children:x.map(s=>e.jsxs(T,{children:[e.jsxs(p,{children:[e.jsx($,{size:14,style:{marginRight:6,verticalAlign:"middle"}}),s.name]}),e.jsx(p,{children:s.platform}),e.jsx(p,{children:e.jsxs("code",{style:{fontSize:"11px"},children:[s.udid.slice(0,14),"…"]})}),e.jsx(p,{style:{textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>q(s.udid),children:"Return to shared pool"})})]},s.udid))})]})]})]})]})]})};export{ve as Teams};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as G,r as o,p as V,X as v}from"./index-
|
|
1
|
+
import{c as G,r as o,p as V,X as v}from"./index-DI1fGIyj.js";import{f as Z}from"./time-D-gjmE1o.js";/**
|
|
2
2
|
* @license lucide-react v0.555.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as t,h as u}from"./index-
|
|
1
|
+
import{r as t,h as u}from"./index-DI1fGIyj.js";function n(){const[s,r]=t.useState(null);return t.useEffect(()=>{let e=!0;return u().then(o=>e&&r(o.passwordReset)),()=>{e=!1}},[]),s}export{n as u};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as w,b as k,r as a,j as e,
|
|
1
|
+
import{c as w,b as k,r as a,j as e,a1 as C}from"./index-DI1fGIyj.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-DMF9Mk5H.js";import{f as U}from"./time-D-gjmE1o.js";import{P}from"./page-header--aTyJXq6.js";import{B as y}from"./button-fIW8owMI.js";import{I as N}from"./input-BjDRKCea.js";import{S as g}from"./select-DvCSv25z.js";import{P as B}from"./plus-CsIk10Cq.js";import{T as L}from"./trash-2-CTZpPSdd.js";import"./utils-CDN07tui.js";/**
|
|
2
2
|
* @license lucide-react v0.555.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{o as s}from"./index-
|
|
1
|
+
import{o as s}from"./index-DI1fGIyj.js";const o="/xenon/api/users";async function c(){const t=await fetch(`${o}/`,{credentials:"include"});if(!t.ok)throw new Error(`listUsers failed (${t.status})`);return t.json()}async function i(t){const e=await fetch(`${o}/`,{method:"POST",credentials:"include",headers:{"Content-Type":"application/json",...s},body:JSON.stringify(t)});if(!e.ok){const n=await e.json().catch(()=>({}));throw new Error(n.error||`createUser failed (${e.status})`)}return e.json()}async function d(t,e){const n=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)});if(!n.ok){const r=await n.json().catch(()=>({}));throw new Error(r.error||`updateUser failed (${n.status})`)}return n.json()}async function l(t){const e=await fetch(`${o}/${encodeURIComponent(t)}`,{method:"DELETE",credentials:"include"});if(!e.ok&&e.status!==204){const n=await e.json().catch(()=>({}));throw new Error(n.error||`deleteUser failed (${e.status})`)}}async function u(t){const e=await fetch(`${o}/${encodeURIComponent(t)}/reset-link`,{method:"POST",credentials:"include"}),n=await e.json().catch(()=>({}));if(!e.ok)throw new Error(n.error||`Reset link failed (${e.status})`);return n}export{i as a,u as c,l as d,c as l,d as u};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{t as P,r,X as c,j as e,O as u,A as b,C as U,P as O}from"./index-DI1fGIyj.js";/* empty css */import{F as S}from"./FieldGroup-Bvn08svv.js";import{P as D}from"./page-header--aTyJXq6.js";import{T as V}from"./trash-2-CTZpPSdd.js";import{P as x}from"./plus-CsIk10Cq.js";import{C as B}from"./circle-x-5Uwquo0h.js";import{C as H}from"./circle-check-zJU80_EF.js";import{Z as I}from"./zap-DRz0tDq5.js";import{A as X}from"./activity-Bdf_uIWl.js";const C=[{id:"device_offline",label:"Device offline",icon:U,tone:"red"},{id:"device_new",label:"New device",icon:x,tone:"green"},{id:"session_failed",label:"Session failed",icon:B,tone:"amber"},{id:"selector_health_digest",label:"Selector health digest",icon:O,tone:"green"}],J=["udid","host","name","sessionId","failureReason","eventType","platform"],ae=()=>{const{toast:o}=P(),[g,T]=r.useState([]),[A,w]=r.useState(!0),[l,j]=r.useState(""),[d,k]=r.useState(["device_offline","session_failed"]),[h,f]=r.useState(!1),[v,N]=r.useState(!1),[n,_]=r.useState(!1),[y,m]=r.useState("");r.useEffect(()=>{p()},[]);const p=async()=>{w(!0);try{const s=await c.getWebhookConfigs();T(s||[])}catch(s){console.error("Failed to load webhook configs",s),o("Failed to load webhooks.","error")}finally{w(!1)}},z=async()=>{if(!l.trim()){o("Webhook URL is required.","error");return}if(d.length===0){o("Select at least one trigger event.","error");return}f(!0);try{await c.addWebhookConfig(l.trim(),d,"slack",y||void 0),j(""),m(""),_(!1),k(["device_offline","session_failed"]),await p(),o("Webhook saved.","success")}catch(s){console.error("Failed to add webhook",s),o(`Failed to add webhook: ${s.message||"unknown error"}`,"error")}finally{f(!1)}},E=async(s,t)=>{if(window.confirm(`Remove webhook for "${t}"?`))try{await c.deleteWebhookConfig(s),await p(),o("Webhook removed.","success")}catch(a){console.error("Failed to delete webhook",a),o(`Failed to delete: ${a.message||"unknown error"}`,"error")}},L=async()=>{if(l.trim()){N(!0);try{await c.testWebhook(l.trim(),"slack"),o("Test payload delivered.","success")}catch(s){o(`Test failed: ${s.message||"check the URL"}`,"error")}finally{N(!1)}}},$=s=>{k(t=>t.includes(s)?t.filter(a=>a!==s):[...t,s])},R=s=>{m(t=>t+`{{${s}}} `)};return e.jsxs("div",{className:"settings-container",children:[e.jsx(D,{icon:u,title:"Notifications",subtitle:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."}),e.jsxs("div",{className:"settings-content",children:[A?e.jsxs("div",{className:"settings-loading",style:{height:200},children:[e.jsx(b,{className:"animate-spin",size:28}),e.jsx("span",{children:"Loading webhooks…"})]}):g.length===0?e.jsxs("div",{className:"empty-state",children:[e.jsx("div",{className:"empty-state__icon",children:e.jsx(u,{size:28})}),e.jsx("h3",{className:"empty-state__title",children:"No webhooks configured"}),e.jsx("p",{className:"empty-state__copy",children:"Wire up a Slack incoming webhook or any HTTPS endpoint to receive alerts when devices go offline, new devices register, or sessions fail. Configure your first webhook below."})]}):e.jsx("div",{className:"webhook-list-grid",children:g.map(s=>{const t=(()=>{try{return JSON.parse(s.events)}catch{return[]}})();return e.jsxs("div",{className:"webhook-row-card",children:[e.jsxs("div",{className:"webhook-row-card__header",children:[e.jsx("span",{className:`pill-chip ${s.payloadTemplate?"pill-chip--admin":"pill-chip--scope"}`,children:s.payloadTemplate?"CUSTOM":"SLACK"}),e.jsx("span",{className:"webhook-row-card__url",title:s.url,children:s.url}),e.jsxs("button",{type:"button",className:"row-action row-action--danger",onClick:()=>E(s.id,s.url),"aria-label":`Remove webhook for ${s.url}`,children:[e.jsx(V,{size:13}),e.jsx("span",{children:"Remove"})]})]}),e.jsx("div",{className:"webhook-row-card__events",children:t.length===0?e.jsx("span",{className:"webhook-row-card__no-events",children:"No triggers"}):t.map(a=>{const i=C.find(F=>F.id===a),W=(i==null?void 0:i.icon)??u;return e.jsxs("span",{className:`event-pill event-pill--${(i==null?void 0:i.tone)||"gray"}`,children:[e.jsx(W,{size:11}),(i==null?void 0:i.label)||a]},a)})})]},s.id)})}),e.jsxs("div",{className:"add-webhook-card",children:[e.jsxs("div",{className:"add-webhook-card__header",children:[e.jsx(x,{size:14}),e.jsx("span",{children:"Add a new webhook"})]}),e.jsxs("div",{className:"add-webhook-card__body",children:[e.jsx(S,{label:"Webhook URL",description:"The endpoint we POST webhook events to.",htmlFor:"webhook-url",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{id:"webhook-url",type:"url",placeholder:"https://hooks.slack.com/services/...",value:l,onChange:s=>j(s.target.value)})})}),e.jsx(S,{label:"Trigger events",children:e.jsx("div",{className:"event-toggle-grid",children:C.map(s=>{const t=d.includes(s.id),a=s.icon;return e.jsxs("button",{type:"button",role:"checkbox","aria-checked":t,className:`event-toggle event-toggle--${s.tone} ${t?"is-selected":""}`,onClick:()=>$(s.id),children:[e.jsx(a,{size:14,className:"event-toggle__lead"}),e.jsx("span",{children:s.label}),t&&e.jsx(H,{size:14,className:"event-toggle__check"})]},s.id)})})}),e.jsxs("div",{className:"template-section",children:[e.jsxs("button",{type:"button",className:"template-section__toggle",onClick:()=>_(!n),"aria-expanded":n,children:[e.jsx(I,{size:14,className:n?"tone-amber":"tone-dim"}),e.jsx("span",{children:"Use custom payload (optional)"}),e.jsx("span",{className:"template-section__indicator",children:n?"−":"+"})]}),n&&e.jsxs("div",{className:"template-editor",children:[e.jsx("p",{className:"template-editor__hint",children:"Define a JSON or text template. Click a variable below to insert it."}),e.jsx("div",{className:"template-editor__chips",children:J.map(s=>e.jsx("button",{type:"button",className:"template-editor__chip",onClick:()=>R(s),children:`{{${s}}}`},s))}),e.jsx("textarea",{className:"template-editor__textarea",placeholder:'Example: { "text": "Alert: Device {{udid}} is offline!" }',value:y,onChange:s=>m(s.target.value),rows:4})]})]})]}),e.jsxs("div",{className:"add-webhook-card__footer",children:[e.jsxs("button",{type:"button",className:"page-header-action page-header-action--ghost",onClick:L,disabled:!l.trim()||v,children:[v?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(X,{size:14}),e.jsx("span",{children:"Test payload"})]}),e.jsxs("button",{type:"button",className:"page-header-action",onClick:z,disabled:h||!l.trim(),children:[h?e.jsx(b,{size:14,className:"animate-spin"}):e.jsx(x,{size:14}),e.jsx("span",{children:h?"Saving…":"Save webhook"})]})]})]})]})]})};export{ae as WebhookSettings};
|
package/lib/public/index.html
CHANGED
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
Learn how to configure a non-root public URL by running `npm run build`.
|
|
37
37
|
-->
|
|
38
38
|
<title>Xenon</title>
|
|
39
|
-
<script type="module" crossorigin src="/xenon/assets/index-
|
|
40
|
-
<link rel="stylesheet" crossorigin href="/xenon/assets/index-
|
|
39
|
+
<script type="module" crossorigin src="/xenon/assets/index-DI1fGIyj.js"></script>
|
|
40
|
+
<link rel="stylesheet" crossorigin href="/xenon/assets/index-Bxoxt9sV.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
|
43
43
|
<body>
|
package/lib/schema.json
CHANGED
|
@@ -288,31 +288,6 @@
|
|
|
288
288
|
"description": "Record CPU and memory every 2 s for each session on this server's own phones (Android: the app under test and the device; iPhone: device CPU), shown in the session page's Performance panel. Needs the dashboard."
|
|
289
289
|
}
|
|
290
290
|
},
|
|
291
|
-
"required": [
|
|
292
|
-
"platform",
|
|
293
|
-
"androidDeviceType",
|
|
294
|
-
"iosDeviceType",
|
|
295
|
-
"skipChromeDownload",
|
|
296
|
-
"maxSessions",
|
|
297
|
-
"deviceAvailabilityTimeoutMs",
|
|
298
|
-
"deviceAvailabilityQueryIntervalMs",
|
|
299
|
-
"sendNodeDevicesToHubIntervalMs",
|
|
300
|
-
"checkStaleDevicesIntervalMs",
|
|
301
|
-
"checkBlockedDevicesIntervalMs",
|
|
302
|
-
"newCommandTimeoutSec",
|
|
303
|
-
"bindHostOrIp",
|
|
304
|
-
"enableDashboard",
|
|
305
|
-
"bootedSimulators",
|
|
306
|
-
"removeDevicesFromDatabaseBeforeRunningThePlugin",
|
|
307
|
-
"healthCheckIntervalMs",
|
|
308
|
-
"enableSelfHealing",
|
|
309
|
-
"buildCleanupDays",
|
|
310
|
-
"buildCleanupMaxCount",
|
|
311
|
-
"buildCleanupSchedule",
|
|
312
|
-
"deleteBuildAssets",
|
|
313
|
-
"sessionHeartbeatIntervalMs",
|
|
314
|
-
"enableJsonLogging"
|
|
315
|
-
],
|
|
316
291
|
"title": "IPluginArgs",
|
|
317
292
|
"type": "object",
|
|
318
293
|
"definitions": {
|
|
@@ -88,6 +88,8 @@ const streamLeave_1 = require("./streamLeave");
|
|
|
88
88
|
const SessionOwnerResolver_1 = require("../../services/device-access/SessionOwnerResolver");
|
|
89
89
|
const actor_1 = require("../../services/device-access/actor");
|
|
90
90
|
const deviceAccessPolicy_1 = require("../../services/device-access/deviceAccessPolicy");
|
|
91
|
+
const leaseHold_1 = require("../../services/device-access/leaseHold");
|
|
92
|
+
const activeLeases_1 = require("../../services/lease/activeLeases");
|
|
91
93
|
/**
|
|
92
94
|
* The answer for an unknown udid, and for another team's phone (see
|
|
93
95
|
* deviceTeamGuard): the same JSON on every route. Most routes used to send
|
|
@@ -623,7 +625,7 @@ const MJPEG_PROXY_CACHE = new Map();
|
|
|
623
625
|
* Start stream endpoint - Initiates WDA and MJPEG streaming for iOS devices
|
|
624
626
|
*/
|
|
625
627
|
router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void 0, function* () {
|
|
626
|
-
var _a, _b, _c, _d;
|
|
628
|
+
var _a, _b, _c, _d, _e;
|
|
627
629
|
const { udid } = req.params;
|
|
628
630
|
// Someone is about to watch: a page that just left (a reload, or another tab
|
|
629
631
|
// closing) must not stop the stream this page is starting.
|
|
@@ -654,6 +656,18 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
654
656
|
return res.status(503).json((0, deviceAccessPolicy_1.ownershipUnavailableBody)());
|
|
655
657
|
}
|
|
656
658
|
}
|
|
659
|
+
// Busy with nothing in session_id is a lease's lock: is it the caller's?
|
|
660
|
+
let leaseHolder = false;
|
|
661
|
+
if (device.busy && !device.session_id && !actor.isAdmin) {
|
|
662
|
+
try {
|
|
663
|
+
const held = yield (0, leaseHold_1.leaseHoldFor)({ udid, host: device.host }, (u, h) => (0, activeLeases_1.activeLeaseOn)(u, h), (id) => ownerResolver.leaseHolderOf(id));
|
|
664
|
+
leaseHolder = (0, deviceAccessPolicy_1.isLeaseHolder)(held, actorUserId, actor.apiKeyId);
|
|
665
|
+
}
|
|
666
|
+
catch (e) {
|
|
667
|
+
logger_1.default.error(`stream/start: lease lookup failed for ${udid}: ${(_b = e === null || e === void 0 ? void 0 : e.message) !== null && _b !== void 0 ? _b : e}`);
|
|
668
|
+
return res.status(503).json((0, deviceAccessPolicy_1.ownershipUnavailableBody)());
|
|
669
|
+
}
|
|
670
|
+
}
|
|
657
671
|
const conflict = (0, streamStartConflict_1.decideStreamStartConflict)({
|
|
658
672
|
udid,
|
|
659
673
|
busy: !!device.busy,
|
|
@@ -663,6 +677,7 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
663
677
|
actorUserId,
|
|
664
678
|
actorApiKeyId: actor.apiKeyId,
|
|
665
679
|
isAdmin: actor.isAdmin,
|
|
680
|
+
leaseHolder,
|
|
666
681
|
});
|
|
667
682
|
if (conflict.action === 'deny') {
|
|
668
683
|
// holderName lookup is cosmetic — a failure here must still deny (never
|
|
@@ -673,7 +688,7 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
673
688
|
holderName = yield ownerResolver.displayName(conflict.holderId);
|
|
674
689
|
}
|
|
675
690
|
catch (e) {
|
|
676
|
-
logger_1.default.warn(`stream/start: holder name lookup failed for ${conflict.holderId}: ${(
|
|
691
|
+
logger_1.default.warn(`stream/start: holder name lookup failed for ${conflict.holderId}: ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e}`);
|
|
677
692
|
}
|
|
678
693
|
}
|
|
679
694
|
logger_1.default.warn(`Manual Control refused for ${udid}: ${conflict.code} (holder=${conflict.holderId || 'unknown'})`);
|
|
@@ -685,11 +700,11 @@ router.post('/:udid/stream/start', (req, res) => __awaiter(void 0, void 0, void
|
|
|
685
700
|
yield (0, device_service_1.unblockDevice)(udid, device.host);
|
|
686
701
|
}
|
|
687
702
|
catch (e) {
|
|
688
|
-
logger_1.default.warn(`Failed to clear orphaned lock on ${udid}: ${(
|
|
703
|
+
logger_1.default.warn(`Failed to clear orphaned lock on ${udid}: ${(_d = e === null || e === void 0 ? void 0 : e.message) !== null && _d !== void 0 ? _d : e}`);
|
|
689
704
|
}
|
|
690
705
|
}
|
|
691
706
|
try {
|
|
692
|
-
const h264Cfg = (0, androidH264Config_1.resolveAndroidH264)((
|
|
707
|
+
const h264Cfg = (0, androidH264Config_1.resolveAndroidH264)((_e = typedi_1.Container.get(PluginContext_1.PluginContext).pluginArgs.streaming) === null || _e === void 0 ? void 0 : _e.androidH264);
|
|
693
708
|
const flagOn = h264Cfg.enabled;
|
|
694
709
|
// A recording device keeps MJPEG preview so we never run screenrecord and
|
|
695
710
|
// the recording's screencap pipeline against the same device at once.
|
|
@@ -21,6 +21,9 @@ function decideStreamStartConflict(i) {
|
|
|
21
21
|
var _a, _b;
|
|
22
22
|
if (!i.busy)
|
|
23
23
|
return PROCEED;
|
|
24
|
+
// Busy only by the caller's own lease: their phone to preview.
|
|
25
|
+
if (i.leaseHolder && !i.sessionId)
|
|
26
|
+
return PROCEED;
|
|
24
27
|
if ((0, manualLock_1.isManualLock)(i.sessionId)) {
|
|
25
28
|
// Orphan: lock persisted but nothing is serving (server restart, crashed
|
|
26
29
|
// stop, H.264 stop that skipped unlock). Any caller may reclaim it.
|
|
@@ -53,6 +53,7 @@ const prisma_1 = require("../prisma");
|
|
|
53
53
|
const manualLock_1 = require("../services/recording/manualLock");
|
|
54
54
|
const deviceVisibility_1 = require("../services/device-access/deviceVisibility");
|
|
55
55
|
const DeviceTeamResolver_1 = require("../services/device-access/DeviceTeamResolver");
|
|
56
|
+
const activeLeases_1 = require("../services/lease/activeLeases");
|
|
56
57
|
// Use a Proxy to ensure we're always using the latest store from the factory,
|
|
57
58
|
// which is critical for test isolation when the factory cache is cleared.
|
|
58
59
|
const store = new Proxy({}, {
|
|
@@ -434,10 +435,49 @@ function releaseClaimOn(device, ref) {
|
|
|
434
435
|
}
|
|
435
436
|
yield (0, device_utils_1.setUtilizationTime)(device.udid, totalUtilization);
|
|
436
437
|
logger_1.default.debug(`Released ${JSON.stringify(ref)} on ${device.udid}`);
|
|
438
|
+
if (yield keepLeaseLock(device))
|
|
439
|
+
return true;
|
|
437
440
|
emitUnblocked(device);
|
|
438
441
|
return true;
|
|
439
442
|
});
|
|
440
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* A session's phone that an SDK lease still holds goes back to the lease,
|
|
446
|
+
* not to the pool: the release above cleared `busy`, which is the lease's
|
|
447
|
+
* only lock (deviceClaims.ts). Allocation would skip the phone anyway, but
|
|
448
|
+
* the device list, the ownership guard and the dashboard read `busy`. The
|
|
449
|
+
* lease's own end (release, or LeaseOrphanSweeper) clears it. True when the
|
|
450
|
+
* phone was kept for its lease.
|
|
451
|
+
*/
|
|
452
|
+
function keepLeaseLock(device) {
|
|
453
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
454
|
+
try {
|
|
455
|
+
if (!(yield (0, activeLeases_1.activeLeaseOn)(device.udid, device.host)))
|
|
456
|
+
return false;
|
|
457
|
+
// lastCmdExecutedAt, so that if this lock outlives its lease (the undo
|
|
458
|
+
// below fails) the idle sweeper still frees it: it skips the phone only
|
|
459
|
+
// while a live lease holds it.
|
|
460
|
+
yield store.updateDevice(device.udid, device.host, {
|
|
461
|
+
busy: true,
|
|
462
|
+
lastCmdExecutedAt: Date.now(),
|
|
463
|
+
});
|
|
464
|
+
// The lease may have ended between the check and the write; its own
|
|
465
|
+
// release ran before ours and cleared nothing. Then undo, by the same
|
|
466
|
+
// conditional clear it uses. A release after this check runs after the
|
|
467
|
+
// write, and clears it itself.
|
|
468
|
+
if (!(yield (0, activeLeases_1.activeLeaseOn)(device.udid, device.host))) {
|
|
469
|
+
yield store.releaseLeaseLock(device.udid, device.host);
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
logger_1.default.debug(`Kept ${device.udid} at ${device.host} busy for its lease`);
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
logger_1.default.warn(`Could not check ${device.udid} for a lease after its session: ${error}`);
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
441
481
|
/** The phone's total use once the session on it now ends. */
|
|
442
482
|
function utilizationAfter(device) {
|
|
443
483
|
var _a;
|
|
@@ -246,7 +246,10 @@ class LokiDeviceStore {
|
|
|
246
246
|
});
|
|
247
247
|
const blockedKeys = new Set(activeLeases.map((l) => `${l.deviceUdid}@${l.deviceHost}`));
|
|
248
248
|
const devices = yield this.getDevices(filterOptions);
|
|
249
|
-
const available = devices.find((d) => !d.busy &&
|
|
249
|
+
const available = devices.find((d) => !d.busy &&
|
|
250
|
+
!d.userBlocked &&
|
|
251
|
+
!blockedKeys.has(`${d.udid}@${d.host}`) &&
|
|
252
|
+
(!options.only || options.only.has(`${d.udid}@${d.host}`)));
|
|
250
253
|
if (available) {
|
|
251
254
|
yield this.updateDevice(available.udid, available.host, Object.assign({ busy: true }, (options.claim ? { claimedAt: Date.now(), claimSessionId: null } : {})));
|
|
252
255
|
return available;
|
|
@@ -323,7 +323,8 @@ class PrismaDeviceStore {
|
|
|
323
323
|
const candidates = yield this.getDevices(Object.assign(Object.assign({}, filterOptions), { busy: false }));
|
|
324
324
|
// 2. Attempt to atomically lock them one by one, skipping lease-blocked devices
|
|
325
325
|
for (const device of candidates) {
|
|
326
|
-
|
|
326
|
+
const key = `${device.udid}@${device.host}`;
|
|
327
|
+
if (blockedKeys.has(key) || (options.only && !options.only.has(key))) {
|
|
327
328
|
continue;
|
|
328
329
|
}
|
|
329
330
|
const now = Date.now();
|