@xenon-device-management/xenon 2.6.0 → 2.6.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/lib/package.json +1 -1
- package/lib/public/assets/{AnnotationOverlay-PMj0TMCJ.js → AnnotationOverlay-U9aJANVi.js} +1 -1
- package/lib/public/assets/{ApiKeyGate-DXo-ecxe.js → ApiKeyGate-6265m84B.js} +1 -1
- package/lib/public/assets/{BugReportButton-DY9ltq27.js → BugReportButton-CGeVEuQ-.js} +1 -1
- package/lib/public/assets/{DeviceMosaicView-CT7nA_re.js → DeviceMosaicView-C_3HwhVZ.js} +1 -1
- package/lib/public/assets/{EmptyState-DX4jwZ9l.js → EmptyState-D5BTZSIa.js} +1 -1
- package/lib/public/assets/{FieldGroup-Czk8gPhv.js → FieldGroup-CDBhSDsT.js} +1 -1
- package/lib/public/assets/{FilterMenu-DTblXm4v.js → FilterMenu-7JEhwNEM.js} +1 -1
- package/lib/public/assets/{Menu-CdGgXICe.js → Menu-DWl5uJ9E.js} +1 -1
- package/lib/public/assets/{Modal-CtzTciQY.js → Modal-BZEfRpq8.js} +1 -1
- package/lib/public/assets/{RecordingPage-BjJH7AyA.js → RecordingPage-B_EK9yKX.js} +1 -1
- package/lib/public/assets/{RecordingsPage-BkgX2xkA.js → RecordingsPage-CptMr6IM.js} +1 -1
- package/lib/public/assets/{SegmentedControl-DEXgl8Qw.js → SegmentedControl-D7BBuUDP.js} +1 -1
- package/lib/public/assets/{SettingCard-BnwAoJhk.js → SettingCard-W3QrpCyh.js} +1 -1
- package/lib/public/assets/{Table-hmNY-ICS.js → Table-BthtE6KF.js} +1 -1
- package/lib/public/assets/{activity-Bmuu2fcZ.js → activity-CdoCkXe1.js} +1 -1
- package/lib/public/assets/{ai-settings-uAzHwRK_.js → ai-settings-CkGIJyKN.js} +1 -1
- package/lib/public/assets/{api-keys-CFZeXLs7.js → api-keys-2lHmq44B.js} +1 -1
- package/lib/public/assets/{apps-DgwQRGg6.js → apps-BCHpRvo_.js} +1 -1
- package/lib/public/assets/{arrow-left-D5x8OLs-.js → arrow-left-CpaUvzcV.js} +1 -1
- package/lib/public/assets/{arrow-right-DRYGEjl5.js → arrow-right-TCqk2-9N.js} +1 -1
- package/lib/public/assets/{arrow-up-right-DLtHJsd6.js → arrow-up-right-B6JBqR-t.js} +1 -1
- package/lib/public/assets/{auth-shell-D_TLCi0Y.js → auth-shell-BSyFjCa8.js} +2 -2
- package/lib/public/assets/builds-page-D0RnbdGV.js +11 -0
- package/lib/public/assets/{button-CbVMa9PL.js → button-CB1ehl9z.js} +1 -1
- package/lib/public/assets/{calendar-DgmwU-Oq.js → calendar-DXZ0-sa6.js} +1 -1
- package/lib/public/assets/{check-Du5vvLoJ.js → check-Bq0QWhtG.js} +1 -1
- package/lib/public/assets/{chevron-left-CiRlzB7w.js → chevron-left-BMI8J11V.js} +1 -1
- package/lib/public/assets/{chevron-right-i93nTPHX.js → chevron-right-tbezHlhW.js} +1 -1
- package/lib/public/assets/{circle-check-DxYO7yp5.js → circle-check-BgkLodxU.js} +1 -1
- package/lib/public/assets/{circle-x-Dw1DFg0I.js → circle-x-DuFD0enL.js} +1 -1
- package/lib/public/assets/{clock-0q-GqRU0.js → clock-C7AhkXMm.js} +1 -1
- package/lib/public/assets/{copy-Ds-F075f.js → copy-CiNhxJMO.js} +1 -1
- package/lib/public/assets/{device-explorer-Dsldlx2g.js → device-explorer-BFWU04bT.js} +1 -1
- package/lib/public/assets/{download-BdJEWL8b.js → download-Di_q2qj5.js} +1 -1
- package/lib/public/assets/{forgot-password-D7eNuw-V.js → forgot-password-gtciaIez.js} +1 -1
- package/lib/public/assets/{funnel-CY3Ovebz.js → funnel-BY1R0ydM.js} +1 -1
- package/lib/public/assets/{index-tW_VASh2.js → index-B1iJx0w1.js} +2 -2
- package/lib/public/assets/{index-BpJwb7cQ.js → index-Bgx65Mb_.js} +1 -1
- package/lib/public/assets/{input-DvCYrL_q.js → input-B83lhHJ_.js} +1 -1
- package/lib/public/assets/{keycodes-CyOBCkU6.js → keycodes-BjiWgQik.js} +1 -1
- package/lib/public/assets/{layers-ChEcsAc2.js → layers-CxMkF86z.js} +1 -1
- package/lib/public/assets/{list-checks-EWcywAEC.js → list-checks-2GnUUcTR.js} +1 -1
- package/lib/public/assets/{lock-DmkRlmQB.js → lock-BRKQ_6ix.js} +1 -1
- package/lib/public/assets/{login-BXeYWAn-.js → login-Chmwsz_d.js} +1 -1
- package/lib/public/assets/{maintenance-settings-Cd37WZ5e.js → maintenance-settings-DEb0LrBA.js} +1 -1
- package/lib/public/assets/{monitor-BTqiDmvk.js → monitor-ClL-76V7.js} +1 -1
- package/lib/public/assets/{mouse-pointer-2-BrwX4M5d.js → mouse-pointer-2-C815rsZl.js} +1 -1
- package/lib/public/assets/{network-8uySyPSM.js → network-CBd_S-GU.js} +1 -1
- package/lib/public/assets/{overview-CdBGYC95.js → overview-B0paJ7DP.js} +1 -1
- package/lib/public/assets/{page-header-BYzd5kfp.js → page-header-D9FkEgQm.js} +1 -1
- package/lib/public/assets/{plus-BIekJsya.js → plus-DF-cFokG.js} +1 -1
- package/lib/public/assets/{profile-page-a-nQ7xFX.js → profile-page-BrdnJBRA.js} +1 -1
- package/lib/public/assets/{reset-password-7Gbj29w1.js → reset-password-DJFBWJnT.js} +1 -1
- package/lib/public/assets/{runbook-page-Bgd9u7W6.js → runbook-page-D5SIpqrf.js} +1 -1
- package/lib/public/assets/{select-DEpRzm_3.js → select-Wc_B9XUe.js} +1 -1
- package/lib/public/assets/{selector-detail-page-CvqjXfvp.js → selector-detail-page-q7-lRo_l.js} +1 -1
- package/lib/public/assets/{selector-health-BEzkwE_K.js → selector-health-Qe-NusTk.js} +1 -1
- package/lib/public/assets/{selector-health-page-Dl_IE1Je.js → selector-health-page-Dw2WUlDJ.js} +1 -1
- package/lib/public/assets/{session-detail-page-CuF_EwRE.js → session-detail-page-bZpHxz1l.js} +3 -3
- package/lib/public/assets/{settings-CxLw5tlF.js → settings-DA_CIMVB.js} +1 -1
- package/lib/public/assets/stat-tile-CxsAGcDU.js +8 -0
- package/lib/public/assets/{tablet-DJBOerD6.js → tablet-_Uwg1m0t.js} +1 -1
- package/lib/public/assets/{teams-CDQQdVRl.js → teams-ZTGk8Wky.js} +1 -1
- package/lib/public/assets/{trash-2-B_zQvjI3.js → trash-2-BcWlaBK8.js} +1 -1
- package/lib/public/assets/{upload-9FV8q3NB.js → upload-47Lf5U7r.js} +1 -1
- package/lib/public/assets/{use-password-reset-mode-Cx5GHMnk.js → use-password-reset-mode-B3cWCSu5.js} +1 -1
- package/lib/public/assets/{users-ep-SOGaV.js → users-DGHET-4G.js} +1 -1
- package/lib/public/assets/{users-DC0IoOuZ.js → users-IiOEsNnr.js} +1 -1
- package/lib/public/assets/{video-off-BmUg-gKW.js → video-off-D-jBA0at.js} +1 -1
- package/lib/public/assets/{webhook-settings-CwUBzLGC.js → webhook-settings-HqV4V6_J.js} +1 -1
- package/lib/public/assets/{zap-CBld_5xf.js → zap-iAfVTWG0.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/services/recording/RecordingOrchestrator.js +35 -50
- package/lib/test/integration/cross-workflow.spec.js +30 -6
- package/lib/test/unit/recording-orchestrator.spec.js +71 -9
- package/lib/test/unit/recording-source-ended.spec.js +17 -4
- package/package.json +1 -1
- package/lib/public/assets/builds-page-CwKR7w5o.js +0 -11
- package/lib/public/assets/stat-tile-DFuoxV5Y.js +0 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as t,h as u}from"./index-
|
|
1
|
+
import{r as t,h as u}from"./index-B1iJx0w1.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 +1 @@
|
|
|
1
|
-
import{o as s}from"./index-
|
|
1
|
+
import{o as s}from"./index-B1iJx0w1.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};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as w,b as k,r as a,j as e,$ as C}from"./index-
|
|
1
|
+
import{c as w,b as k,r as a,j as e,$ as C}from"./index-B1iJx0w1.js";import{l as M,d as R,c as I,a as A,u as D}from"./users-DGHET-4G.js";import{f as U}from"./time-B8FIgwPE.js";import{P}from"./page-header-D9FkEgQm.js";import{B as y}from"./button-CB1ehl9z.js";import{I as N}from"./input-B83lhHJ_.js";import{S as g}from"./select-Wc_B9XUe.js";import{P as B}from"./plus-DF-cFokG.js";import{T as L}from"./trash-2-BcWlaBK8.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{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-
|
|
1
|
+
import{t as P,r,X as c,j as e,J as u,A as b,C as U,K as D}from"./index-B1iJx0w1.js";/* empty css */import{F as S}from"./FieldGroup-CDBhSDsT.js";import{P as O}from"./page-header-D9FkEgQm.js";import{T as V}from"./trash-2-BcWlaBK8.js";import{P as x}from"./plus-DF-cFokG.js";import{C as B}from"./circle-x-DuFD0enL.js";import{C as H}from"./circle-check-BgkLodxU.js";import{Z as I}from"./zap-iAfVTWG0.js";import{A as J}from"./activity-CdoCkXe1.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:D,tone:"green"}],X=["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(O,{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:X.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(J,{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,7 +36,7 @@
|
|
|
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-
|
|
39
|
+
<script type="module" crossorigin src="/xenon/assets/index-B1iJx0w1.js"></script>
|
|
40
40
|
<link rel="stylesheet" crossorigin href="/xenon/assets/index-C8pvxG1f.css">
|
|
41
41
|
</head>
|
|
42
42
|
|
|
@@ -135,9 +135,22 @@ class RecordingError extends Error {
|
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
exports.RecordingError = RecordingError;
|
|
138
|
+
/**
|
|
139
|
+
* The live preview's leave path, as a page that stops watching uses it: after
|
|
140
|
+
* a short grace, the phone's preview is stopped and its hold released unless
|
|
141
|
+
* someone watches it or another recording reads it. Imported when called:
|
|
142
|
+
* control.ts is a router, loaded long after this service.
|
|
143
|
+
*/
|
|
144
|
+
function leaveDevice(udid) {
|
|
145
|
+
void Promise.resolve().then(() => __importStar(require('../../app/routers/control'))).then(({ previewLeaves }) => previewLeaves.leave(udid))
|
|
146
|
+
.catch((err) => {
|
|
147
|
+
var _a;
|
|
148
|
+
recLog.warn(`Could not hand ${udid} to the preview's leave path: ${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
138
151
|
let RecordingOrchestrator = class RecordingOrchestrator {
|
|
139
152
|
constructor(deps = {}) {
|
|
140
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
153
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
141
154
|
/**
|
|
142
155
|
* Recording ids currently being closed out. Both the user-initiated stop and
|
|
143
156
|
* an ffmpeg self-exit finalize rows, and they can land at the same instant —
|
|
@@ -158,7 +171,8 @@ let RecordingOrchestrator = class RecordingOrchestrator {
|
|
|
158
171
|
this.ensureMjpegPortFn = (_h = deps.ensureMjpegPortFn) !== null && _h !== void 0 ? _h : ensureMjpegForRecording_1.ensureMjpegForRecording;
|
|
159
172
|
this.nodeSourceFn = (_j = deps.nodeSourceFn) !== null && _j !== void 0 ? _j : ((udid, actorId) => (0, nodeRecordingSource_1.recordingSourceFor)(udid, actorId));
|
|
160
173
|
this.probeDurationMsFn = (_k = deps.probeDurationMsFn) !== null && _k !== void 0 ? _k : probeDuration_1.probeVideoDurationMs;
|
|
161
|
-
this.
|
|
174
|
+
this.leaveDeviceFn = (_l = deps.leaveDeviceFn) !== null && _l !== void 0 ? _l : leaveDevice;
|
|
175
|
+
this.now = (_m = deps.now) !== null && _m !== void 0 ? _m : Date.now;
|
|
162
176
|
}
|
|
163
177
|
get busyPrecheck() {
|
|
164
178
|
return this._deps.busyPrecheck;
|
|
@@ -398,6 +412,7 @@ let RecordingOrchestrator = class RecordingOrchestrator {
|
|
|
398
412
|
failReason = (_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : 'ffmpeg_stop_failed';
|
|
399
413
|
}
|
|
400
414
|
// Its ffmpeg is done reading; a group's composite stops before this.
|
|
415
|
+
const fromNode = this.nodeSources.has(r.id);
|
|
401
416
|
yield this.closeNodeSource(r.id);
|
|
402
417
|
// How long the video actually is, not how long the session was open. Those
|
|
403
418
|
// agree only while capture keeps up: a recording whose device died at 26s
|
|
@@ -444,7 +459,7 @@ let RecordingOrchestrator = class RecordingOrchestrator {
|
|
|
444
459
|
recLog.warn(`finalize failed for ${r.id}: ${err === null || err === void 0 ? void 0 : err.message}`);
|
|
445
460
|
}
|
|
446
461
|
this.gate.release(r.id);
|
|
447
|
-
yield this.
|
|
462
|
+
yield this.letGoOfDevice(r.device_udid, (_c = r.device_host) !== null && _c !== void 0 ? _c : '127.0.0.1', fromNode);
|
|
448
463
|
if (durationMs !== undefined) {
|
|
449
464
|
durationHistogram.record(durationMs, {
|
|
450
465
|
outcome: status === 'STOPPED' ? attributes_1.OUTCOME.SUCCESS : attributes_1.OUTCOME.FAILURE,
|
|
@@ -647,57 +662,27 @@ let RecordingOrchestrator = class RecordingOrchestrator {
|
|
|
647
662
|
}))();
|
|
648
663
|
}
|
|
649
664
|
/**
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
*
|
|
665
|
+
* Let go of a phone whose recording has ended.
|
|
666
|
+
*
|
|
667
|
+
* This server's own phone goes to the live preview's leave path, as a page
|
|
668
|
+
* that stops watching it does. After a short grace, which also lets this
|
|
669
|
+
* recording's own ffmpeg connection drop, its preview is stopped and its
|
|
670
|
+
* hold released, unless someone watches it or another recording reads it.
|
|
671
|
+
* Whether its stream is running says nothing: the recording starts the
|
|
672
|
+
* stream itself when none runs (ensureMjpegForRecording). Taking that
|
|
673
|
+
* stream for a preview's kept the phone held, with nobody watching, until
|
|
674
|
+
* the stream's idle watchdog ten minutes later.
|
|
675
|
+
*
|
|
676
|
+
* A node's phone has no stream on this server, so nobody here watches it:
|
|
677
|
+
* its hold goes at once.
|
|
655
678
|
*/
|
|
656
|
-
|
|
679
|
+
letGoOfDevice(udid, host, fromNode) {
|
|
657
680
|
return __awaiter(this, void 0, void 0, function* () {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
// Lazy import — keeps the orchestrator usable in tests that don't
|
|
661
|
-
// wire stream services into the DI container.
|
|
662
|
-
const { default: IOSStreamService } = yield Promise.resolve().then(() => __importStar(require('../../device-managers/ios/IOSStreamService')));
|
|
663
|
-
const ios = typedi_1.Container.get(IOSStreamService);
|
|
664
|
-
const iosSession = ios.getStreamStatus(udid);
|
|
665
|
-
if ((iosSession === null || iosSession === void 0 ? void 0 : iosSession.status) === 'running')
|
|
666
|
-
mosaicStreamRunning = true;
|
|
667
|
-
}
|
|
668
|
-
catch (_a) {
|
|
669
|
-
/* ignore — service may not be registered in this context */
|
|
670
|
-
}
|
|
671
|
-
if (!mosaicStreamRunning) {
|
|
672
|
-
try {
|
|
673
|
-
const { default: AndroidStreamService } = yield Promise.resolve().then(() => __importStar(require('../../device-managers/android/AndroidStreamService')));
|
|
674
|
-
const android = typedi_1.Container.get(AndroidStreamService);
|
|
675
|
-
const aSession = android.getStreamStatus(udid);
|
|
676
|
-
if ((aSession === null || aSession === void 0 ? void 0 : aSession.status) === 'running')
|
|
677
|
-
mosaicStreamRunning = true;
|
|
678
|
-
}
|
|
679
|
-
catch (_b) {
|
|
680
|
-
/* ignore */
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
// H.264 preview (androidH264) has no MJPEG session — check it too or we
|
|
684
|
-
// drop the mosaic lock underneath a live WebCodecs tile.
|
|
685
|
-
if (!mosaicStreamRunning) {
|
|
686
|
-
try {
|
|
687
|
-
const { default: AndroidH264StreamService } = yield Promise.resolve().then(() => __importStar(require('../../device-managers/android/AndroidH264StreamService')));
|
|
688
|
-
if (typedi_1.Container.get(AndroidH264StreamService).getMultiplexer(udid)) {
|
|
689
|
-
mosaicStreamRunning = true;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
catch (_c) {
|
|
693
|
-
/* ignore */
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
if (mosaicStreamRunning) {
|
|
697
|
-
recLog.info(`Recording stop: keeping manual lock on ${udid} because the mosaic preview is still active.`);
|
|
681
|
+
if (fromNode) {
|
|
682
|
+
yield this.tryUnblock(udid, host);
|
|
698
683
|
return;
|
|
699
684
|
}
|
|
700
|
-
|
|
685
|
+
this.leaveDeviceFn(udid);
|
|
701
686
|
});
|
|
702
687
|
}
|
|
703
688
|
addBookmark(groupId, recordingId, timecodeMs, label, note) {
|
|
@@ -51,6 +51,7 @@ const sinon_1 = __importDefault(require("sinon"));
|
|
|
51
51
|
const RecordingOrchestrator_1 = require("../../src/services/recording/RecordingOrchestrator");
|
|
52
52
|
const concurrency_gate_1 = require("../../src/services/recording/concurrency-gate");
|
|
53
53
|
const deviceStoreModule = __importStar(require("../../src/data-service/device-store"));
|
|
54
|
+
const artifact_store_1 = require("../helpers/artifact-store");
|
|
54
55
|
/**
|
|
55
56
|
* The six scenarios from the spec's cross-workflow risk matrix
|
|
56
57
|
* (docs/superpowers/specs/2026-04-27-multi-device-mosaic-and-proof-pack-design.md
|
|
@@ -58,7 +59,7 @@ const deviceStoreModule = __importStar(require("../../src/data-service/device-st
|
|
|
58
59
|
* workflow is broken by the new free-form recording surface.
|
|
59
60
|
*/
|
|
60
61
|
function makeOrch(overrides = {}) {
|
|
61
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
62
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
62
63
|
const busyPrecheck = (_a = overrides.busyPrecheck) !== null && _a !== void 0 ? _a : { findBusy: sinon_1.default.stub().resolves([]) };
|
|
63
64
|
const store = (_b = overrides.store) !== null && _b !== void 0 ? _b : {
|
|
64
65
|
create: sinon_1.default.stub().callsFake((i) => __awaiter(this, void 0, void 0, function* () { return (Object.assign({ id: `rec-${i.deviceUdid}` }, i)); })),
|
|
@@ -73,7 +74,14 @@ function makeOrch(overrides = {}) {
|
|
|
73
74
|
};
|
|
74
75
|
const blockDeviceFn = (_e = overrides.blockDeviceFn) !== null && _e !== void 0 ? _e : sinon_1.default.stub().resolves();
|
|
75
76
|
const unblockDeviceFn = (_f = overrides.unblockDeviceFn) !== null && _f !== void 0 ? _f : sinon_1.default.stub().resolves();
|
|
76
|
-
|
|
77
|
+
// Each phone is this server's own, read from a live MJPEG port: the real
|
|
78
|
+
// ones would look the phone up and start its stream.
|
|
79
|
+
const nodeSourceFn = (_g = overrides.nodeSourceFn) !== null && _g !== void 0 ? _g : sinon_1.default.stub().resolves(null);
|
|
80
|
+
const ensureMjpegPortFn = (_h = overrides.ensureMjpegPortFn) !== null && _h !== void 0 ? _h : sinon_1.default.stub().resolves(9100);
|
|
81
|
+
// Where a stopped recording hands its phone: the preview's leave path,
|
|
82
|
+
// which releases the hold once nobody watches it.
|
|
83
|
+
const leaveDeviceFn = (_j = overrides.leaveDeviceFn) !== null && _j !== void 0 ? _j : sinon_1.default.stub();
|
|
84
|
+
const eventMgr = (_k = overrides.eventMgr) !== null && _k !== void 0 ? _k : {
|
|
77
85
|
emitRecordingStarted: sinon_1.default.stub(),
|
|
78
86
|
emitRecordingStopped: sinon_1.default.stub(),
|
|
79
87
|
emitRecordingFailed: sinon_1.default.stub(),
|
|
@@ -88,10 +96,26 @@ function makeOrch(overrides = {}) {
|
|
|
88
96
|
blockDeviceFn,
|
|
89
97
|
eventMgr: eventMgr,
|
|
90
98
|
unblockDeviceFn,
|
|
99
|
+
nodeSourceFn,
|
|
100
|
+
ensureMjpegPortFn,
|
|
101
|
+
leaveDeviceFn,
|
|
91
102
|
});
|
|
92
|
-
return {
|
|
103
|
+
return {
|
|
104
|
+
orch,
|
|
105
|
+
busyPrecheck,
|
|
106
|
+
store,
|
|
107
|
+
gate,
|
|
108
|
+
videoPipeline,
|
|
109
|
+
blockDeviceFn,
|
|
110
|
+
unblockDeviceFn,
|
|
111
|
+
leaveDeviceFn,
|
|
112
|
+
eventMgr,
|
|
113
|
+
};
|
|
93
114
|
}
|
|
94
115
|
describe('Cross-workflow integration: manual + automation safety (6 scenarios)', () => {
|
|
116
|
+
// start() resolves each recording's file through the ArtifactStore, as the
|
|
117
|
+
// server registers it at boot.
|
|
118
|
+
(0, artifact_store_1.useArtifactStore)();
|
|
95
119
|
let factoryStub;
|
|
96
120
|
beforeEach(() => {
|
|
97
121
|
factoryStub = sinon_1.default
|
|
@@ -150,8 +174,8 @@ describe('Cross-workflow integration: manual + automation safety (6 scenarios)',
|
|
|
150
174
|
const passedSessionIds = calls.map((c) => c.args[0].sessionId);
|
|
151
175
|
(0, chai_1.expect)(new Set(passedSessionIds).size).to.equal(2);
|
|
152
176
|
}));
|
|
153
|
-
it('4. ffmpeg stop throws (simulated device unplug) — Recording marked FAILED,
|
|
154
|
-
const { orch, store,
|
|
177
|
+
it('4. ffmpeg stop throws (simulated device unplug) — Recording marked FAILED, phone handed on to release its block', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
|
+
const { orch, store, leaveDeviceFn } = makeOrch({
|
|
155
179
|
store: {
|
|
156
180
|
listGroup: sinon_1.default.stub().resolves([
|
|
157
181
|
{
|
|
@@ -172,7 +196,7 @@ describe('Cross-workflow integration: manual + automation safety (6 scenarios)',
|
|
|
172
196
|
yield orch.stop('grp-X');
|
|
173
197
|
const finalizeArgs = store.finalize.firstCall.args[1];
|
|
174
198
|
(0, chai_1.expect)(finalizeArgs.status).to.equal('FAILED');
|
|
175
|
-
(0, chai_1.expect)(
|
|
199
|
+
(0, chai_1.expect)(leaveDeviceFn.calledOnceWith('U1')).to.equal(true);
|
|
176
200
|
}));
|
|
177
201
|
it('5. server restart with orphan RECORDING row — recoverOnBoot marks FAILED + releases block', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
178
202
|
const { orch, store, unblockDeviceFn } = makeOrch({
|
|
@@ -60,7 +60,7 @@ const fs = __importStar(require("fs"));
|
|
|
60
60
|
const os = __importStar(require("os"));
|
|
61
61
|
const path = __importStar(require("path"));
|
|
62
62
|
function makeOrch(overrides = {}) {
|
|
63
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
63
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
64
64
|
const busyPrecheck = (_a = overrides.busyPrecheck) !== null && _a !== void 0 ? _a : { findBusy: sinon_1.default.stub().resolves([]) };
|
|
65
65
|
const store = (_b = overrides.store) !== null && _b !== void 0 ? _b : {
|
|
66
66
|
create: sinon_1.default.stub().callsFake((i) => __awaiter(this, void 0, void 0, function* () { return (Object.assign({ id: `rec-${i.deviceUdid}` }, i)); })),
|
|
@@ -82,7 +82,9 @@ function makeOrch(overrides = {}) {
|
|
|
82
82
|
const ensureMjpegPortFn = (_g = overrides.ensureMjpegPortFn) !== null && _g !== void 0 ? _g : sinon_1.default.stub().callsFake(() => __awaiter(this, void 0, void 0, function* () { return 9100; }));
|
|
83
83
|
// This server's own phones, unless a test says otherwise.
|
|
84
84
|
const nodeSourceFn = (_h = overrides.nodeSourceFn) !== null && _h !== void 0 ? _h : sinon_1.default.stub().resolves(null);
|
|
85
|
-
|
|
85
|
+
// Where a stopped recording hands its phone: the preview's leave path.
|
|
86
|
+
const leaveDeviceFn = (_j = overrides.leaveDeviceFn) !== null && _j !== void 0 ? _j : sinon_1.default.stub();
|
|
87
|
+
const eventMgr = (_k = overrides.eventMgr) !== null && _k !== void 0 ? _k : {
|
|
86
88
|
emitRecordingStarted: sinon_1.default.stub(),
|
|
87
89
|
emitRecordingStopped: sinon_1.default.stub(),
|
|
88
90
|
emitRecordingFailed: sinon_1.default.stub(),
|
|
@@ -99,6 +101,7 @@ function makeOrch(overrides = {}) {
|
|
|
99
101
|
unblockDeviceFn,
|
|
100
102
|
ensureMjpegPortFn,
|
|
101
103
|
nodeSourceFn,
|
|
104
|
+
leaveDeviceFn,
|
|
102
105
|
now: overrides.now,
|
|
103
106
|
});
|
|
104
107
|
return {
|
|
@@ -112,6 +115,7 @@ function makeOrch(overrides = {}) {
|
|
|
112
115
|
eventMgr,
|
|
113
116
|
ensureMjpegPortFn,
|
|
114
117
|
nodeSourceFn,
|
|
118
|
+
leaveDeviceFn,
|
|
115
119
|
};
|
|
116
120
|
}
|
|
117
121
|
// On a hub, a node's phone is recorded from a loopback relay to its node's
|
|
@@ -181,6 +185,17 @@ describe('RecordingOrchestrator: a node’s phone', () => {
|
|
|
181
185
|
yield orch.stop(groupId);
|
|
182
186
|
(0, chai_1.expect)(relay.close.calledOnce).to.equal(true);
|
|
183
187
|
}));
|
|
188
|
+
// This server runs no stream of a node's phone, so nobody here can be
|
|
189
|
+
// watching it: its hold goes as soon as the recording is finalized.
|
|
190
|
+
it('releases a node phone’s hold at once, not through the leave path', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
191
|
+
const relay = relayFor('N1', 5555);
|
|
192
|
+
const store = rowStore();
|
|
193
|
+
const { orch, unblockDeviceFn, leaveDeviceFn } = makeOrch({ nodeSourceFn: relay.fn, store });
|
|
194
|
+
const { groupId } = yield orch.start({ udids: ['N1'], actorId: 'alice' });
|
|
195
|
+
yield orch.stop(groupId);
|
|
196
|
+
(0, chai_1.expect)(unblockDeviceFn.calledWith('N1', 'h')).to.equal(true);
|
|
197
|
+
(0, chai_1.expect)(leaveDeviceFn.called).to.equal(false);
|
|
198
|
+
}));
|
|
184
199
|
it('closes the relay when that phone’s recording can’t start', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
185
200
|
const relay = relayFor('N1', 5555);
|
|
186
201
|
const videoPipeline = {
|
|
@@ -371,7 +386,7 @@ describe('RecordingOrchestrator.start', () => {
|
|
|
371
386
|
describe('RecordingOrchestrator.stop', () => {
|
|
372
387
|
// compositeOutputPath and per-device paths resolve through ArtifactStore.
|
|
373
388
|
(0, artifact_store_1.useArtifactStore)();
|
|
374
|
-
it('finalizes, releases gate,
|
|
389
|
+
it('finalizes, releases gate, hands each phone to the leave path, emits stopped', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
375
390
|
const store = {
|
|
376
391
|
listGroup: sinon_1.default.stub().resolves([
|
|
377
392
|
{
|
|
@@ -399,15 +414,63 @@ describe('RecordingOrchestrator.stop', () => {
|
|
|
399
414
|
};
|
|
400
415
|
const unblockDeviceFn = sinon_1.default.stub().resolves();
|
|
401
416
|
const eventMgr = { emitRecordingStopped: sinon_1.default.stub() };
|
|
402
|
-
const { orch } = makeOrch({
|
|
417
|
+
const { orch, leaveDeviceFn } = makeOrch({
|
|
418
|
+
store,
|
|
419
|
+
gate,
|
|
420
|
+
videoPipeline,
|
|
421
|
+
unblockDeviceFn,
|
|
422
|
+
eventMgr,
|
|
423
|
+
});
|
|
403
424
|
yield orch.stop('grp-1');
|
|
404
425
|
(0, chai_1.expect)(videoPipeline.stopRecording.callCount).to.equal(2);
|
|
405
426
|
(0, chai_1.expect)(store.finalize.callCount).to.equal(2);
|
|
406
427
|
(0, chai_1.expect)(gate.release.callCount).to.equal(2);
|
|
407
|
-
|
|
428
|
+
// The leave path releases each hold once nobody watches the phone; the
|
|
429
|
+
// recording doesn't release it itself.
|
|
430
|
+
(0, chai_1.expect)(leaveDeviceFn.getCalls().map((c) => c.args[0])).to.deep.equal(['U1', 'U2']);
|
|
431
|
+
(0, chai_1.expect)(unblockDeviceFn.called).to.equal(false);
|
|
408
432
|
(0, chai_1.expect)(eventMgr.emitRecordingStopped.callCount).to.equal(1);
|
|
409
433
|
}));
|
|
410
|
-
|
|
434
|
+
// A recording starts its phone's stream when none runs
|
|
435
|
+
// (ensureMjpegForRecording). Taking that running stream for a preview kept
|
|
436
|
+
// the phone held until the stream's idle watchdog, ten minutes later.
|
|
437
|
+
it('hands the phone to the leave path even while its stream runs', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
438
|
+
const { default: AndroidStreamService } = yield Promise.resolve().then(() => __importStar(require('../../src/device-managers/android/AndroidStreamService')));
|
|
439
|
+
const had = typedi_1.Container.has(AndroidStreamService);
|
|
440
|
+
const previous = had ? typedi_1.Container.get(AndroidStreamService) : undefined;
|
|
441
|
+
typedi_1.Container.set(AndroidStreamService, {
|
|
442
|
+
getStreamStatus: () => ({ status: 'running', viewerCount: 0 }),
|
|
443
|
+
});
|
|
444
|
+
try {
|
|
445
|
+
const store = {
|
|
446
|
+
listGroup: sinon_1.default.stub().resolves([
|
|
447
|
+
{
|
|
448
|
+
id: 'r1',
|
|
449
|
+
device_udid: 'U1',
|
|
450
|
+
device_host: '127.0.0.1',
|
|
451
|
+
file_path: '/nonexistent/r1.mp4',
|
|
452
|
+
started_at: new Date(Date.now() - 5000),
|
|
453
|
+
},
|
|
454
|
+
]),
|
|
455
|
+
finalize: sinon_1.default.stub().resolves({}),
|
|
456
|
+
};
|
|
457
|
+
const videoPipeline = {
|
|
458
|
+
startRecording: sinon_1.default.stub(),
|
|
459
|
+
stopRecording: sinon_1.default.stub().resolves('/tmp/x.mp4'),
|
|
460
|
+
stopComposite: sinon_1.default.stub().resolves(null),
|
|
461
|
+
};
|
|
462
|
+
const { orch, leaveDeviceFn } = makeOrch({ store, videoPipeline });
|
|
463
|
+
yield orch.stop('grp-1');
|
|
464
|
+
(0, chai_1.expect)(leaveDeviceFn.calledOnceWith('U1')).to.equal(true);
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
if (had)
|
|
468
|
+
typedi_1.Container.set(AndroidStreamService, previous);
|
|
469
|
+
else
|
|
470
|
+
typedi_1.Container.remove(AndroidStreamService);
|
|
471
|
+
}
|
|
472
|
+
}));
|
|
473
|
+
it('marks FAILED when ffmpeg stop throws but still hands the phone on', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
411
474
|
const store = {
|
|
412
475
|
listGroup: sinon_1.default.stub().resolves([
|
|
413
476
|
{
|
|
@@ -425,11 +488,10 @@ describe('RecordingOrchestrator.stop', () => {
|
|
|
425
488
|
stopRecording: sinon_1.default.stub().rejects(new Error('ffmpeg died')),
|
|
426
489
|
stopComposite: sinon_1.default.stub().resolves(null),
|
|
427
490
|
};
|
|
428
|
-
const
|
|
429
|
-
const { orch } = makeOrch({ store, videoPipeline, unblockDeviceFn });
|
|
491
|
+
const { orch, leaveDeviceFn } = makeOrch({ store, videoPipeline });
|
|
430
492
|
yield orch.stop('grp-1');
|
|
431
493
|
(0, chai_1.expect)(store.finalize.firstCall.args[1].status).to.equal('FAILED');
|
|
432
|
-
(0, chai_1.expect)(
|
|
494
|
+
(0, chai_1.expect)(leaveDeviceFn.calledOnceWith('U1')).to.equal(true);
|
|
433
495
|
}));
|
|
434
496
|
});
|
|
435
497
|
describe('RecordingOrchestrator.recoverOnBoot', () => {
|
|
@@ -54,6 +54,8 @@ function makeOrch(row, overrides = {}) {
|
|
|
54
54
|
emitRecordingAnnotation: sinon_1.default.stub(),
|
|
55
55
|
};
|
|
56
56
|
const unblockDeviceFn = sinon_1.default.stub().resolves();
|
|
57
|
+
// Where an ended recording hands its phone: the preview's leave path.
|
|
58
|
+
const leaveDeviceFn = sinon_1.default.stub();
|
|
57
59
|
// Never spawn ffmpeg from a unit test; individual cases override this.
|
|
58
60
|
const probeDurationMsFn = (_c = overrides.probeDurationMsFn) !== null && _c !== void 0 ? _c : sinon_1.default.stub().resolves(undefined);
|
|
59
61
|
const orch = new RecordingOrchestrator_1.RecordingOrchestrator({
|
|
@@ -66,8 +68,18 @@ function makeOrch(row, overrides = {}) {
|
|
|
66
68
|
blockDeviceFn: sinon_1.default.stub().resolves(),
|
|
67
69
|
unblockDeviceFn,
|
|
68
70
|
ensureMjpegPortFn: sinon_1.default.stub().resolves(9100),
|
|
71
|
+
leaveDeviceFn,
|
|
69
72
|
});
|
|
70
|
-
return {
|
|
73
|
+
return {
|
|
74
|
+
orch,
|
|
75
|
+
store,
|
|
76
|
+
gate,
|
|
77
|
+
videoPipeline,
|
|
78
|
+
eventMgr,
|
|
79
|
+
unblockDeviceFn,
|
|
80
|
+
leaveDeviceFn,
|
|
81
|
+
probeDurationMsFn,
|
|
82
|
+
};
|
|
71
83
|
}
|
|
72
84
|
describe('RecordingOrchestrator: ffmpeg exiting on its own', () => {
|
|
73
85
|
// The in-memory stores, also when this file runs on its own: without
|
|
@@ -106,13 +118,14 @@ describe('RecordingOrchestrator: ffmpeg exiting on its own', () => {
|
|
|
106
118
|
(0, chai_1.expect)(store.finalize.firstCall.args[1].status).to.equal('FAILED');
|
|
107
119
|
(0, chai_1.expect)(store.finalize.firstCall.args[1].failReason).to.equal('empty_or_corrupt_mp4');
|
|
108
120
|
}));
|
|
109
|
-
it('releases the gate slot and the
|
|
121
|
+
it('releases the gate slot and hands the phone on to release its hold', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
122
|
const row = makeRow();
|
|
111
|
-
const { orch, gate,
|
|
123
|
+
const { orch, gate, leaveDeviceFn } = makeOrch(row);
|
|
112
124
|
gate.tryAcquire(['rec-1']);
|
|
113
125
|
yield orch.handleSourceEnded('rec-1', 0);
|
|
114
126
|
(0, chai_1.expect)(gate.tryAcquire(['rec-1']), 'slot must be free again').to.be.true;
|
|
115
|
-
(0, chai_1.expect)(
|
|
127
|
+
(0, chai_1.expect)(leaveDeviceFn.calledOnceWith(row.device_udid), 'phone handed to the leave path').to.be
|
|
128
|
+
.true;
|
|
116
129
|
}));
|
|
117
130
|
it('does nothing when the row was already finalized', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
118
131
|
// A manual Stop that won the race already closed it out.
|
package/package.json
CHANGED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import{c as te,r as h,p as se,X as H,j as e,z as X,R as M,f as ae,T as re,q as ne,u as le,i as oe,t as ie,M as ce}from"./index-tW_VASh2.js";import{s as de,b as I,P as xe,T as E,i as ue,f as q,a as V,c as me,d as pe,p as he,e as fe,r as ve,C as be,g as ge,h as je,j as D,k as Ne,l as W,S as w,m as ye,n as we,u as Se,o as Ce}from"./stat-tile-DFuoxV5Y.js";import{I as J}from"./input-DvCYrL_q.js";import{L as $e}from"./layers-ChEcsAc2.js";import{B as P}from"./button-CbVMa9PL.js";import{D as ke}from"./download-BdJEWL8b.js";import{s as Re}from"./labels-C21SA4JG.js";import{C as Te}from"./circle-x-Dw1DFg0I.js";import{C as Ee}from"./circle-check-DxYO7yp5.js";import{C as Be}from"./chevron-right-i93nTPHX.js";import{T as Me}from"./tablet-DJBOerD6.js";import{M as Ie}from"./monitor-BTqiDmvk.js";import{P as Le}from"./page-header-BYzd5kfp.js";import{S as _e}from"./select-DEpRzm_3.js";import{E as F}from"./EmptyState-DX4jwZ9l.js";import{L as De}from"./list-checks-EWcywAEC.js";import"./time-B8FIgwPE.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 Oe=[["path",{d:"M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8",key:"14sxne"}],["path",{d:"M3 3v5h5",key:"1xhq8a"}],["path",{d:"M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16",key:"1hlbsb"}],["path",{d:"M16 16h5v5",key:"ccwih5"}]],Ae=te("refresh-ccw",Oe),Pe=15e3;function Fe(t,a){const[s,r]=h.useState(null),x=h.useRef(!0),c=h.useRef(0),{on:u}=se(),o=h.useCallback(async()=>{const d=++c.current;try{const m=await H.getSessionSummary(a?{buildId:a}:{since:de(t,Date.now())});x.current&&d===c.current&&m&&m.current&&r(m)}catch{}},[t,a]);return h.useEffect(()=>{x.current=!0,r(null),o();const d=setInterval(o,Pe);return()=>{x.current=!1,clearInterval(d)}},[o]),h.useEffect(()=>{const d=[u("session_started",o),u("session_stopped",o)];return()=>{for(const m of d)m()}},[u,o]),s}const U=(t,a)=>`${t} ${a}${t===1?"":"s"}`,Ue=({build:t})=>{const{passedCount:a,failedCount:s,runningCount:r,sessionCount:x}=t,c=Math.max(0,x-a-s-r);if(x===0)return null;const u=[a>0&&`${a} passed`,s>0&&`${s} failed`,r>0&&`${r} running`,c>0&&`${c} without a verdict`].filter(Boolean);return e.jsxs("div",{role:"img","aria-label":u.join(", "),title:u.join(", "),className:"mt-2 flex h-1 w-full gap-px overflow-hidden rounded-full bg-[rgb(var(--rgb-fg)/0.08)]",children:[a>0&&e.jsx("span",{className:"bg-[var(--color-success)]",style:{flexGrow:a}}),s>0&&e.jsx("span",{className:"bg-[var(--color-danger)]",style:{flexGrow:s}}),r>0&&e.jsx("span",{className:"bg-[var(--color-warning)]",style:{flexGrow:r}}),c>0&&e.jsx("span",{style:{flexGrow:c}})]})},z=({active:t,onClick:a,children:s})=>e.jsxs("button",{type:"button",onClick:a,"aria-current":t?"true":void 0,className:`w-full text-left relative px-4 py-2.5 transition-colors ${t?"bg-[var(--surface-2)]":"hover:bg-[rgb(var(--rgb-fg)/0.04)]"}`,children:[t&&e.jsx("span",{className:"absolute left-0 top-2 bottom-2 w-[3px] rounded-r bg-[var(--color-accent)]"}),s]}),ze=({builds:t,selectedBuildId:a,onSelect:s,timeFilter:r})=>{const[x,c]=h.useState(""),u=x.trim().toLowerCase(),o=Date.now(),d=t.filter(n=>{if(u&&!`${I(n)} ${n.name??""} ${n.id}`.toLowerCase().includes(u))return!1;if(r!=="all"){const l=Date.parse(String(n.createdAt));if(!Number.isFinite(l)||o-l>xe[r])return!1}return!0}),m=u?"No builds match.":r!=="all"?`No builds in the ${E[r].toLowerCase()}.`:"No builds yet. They appear here after your first test run.";return e.jsxs("aside",{className:"w-[280px] shrink-0 border-r border-[var(--border)] bg-[var(--surface)] flex flex-col min-h-0",children:[e.jsx("div",{className:"p-3 border-b border-[var(--border)]",children:e.jsxs("div",{className:"relative",children:[e.jsx(X,{className:"absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--text-dim)]"}),e.jsx(J,{type:"text",value:x,onChange:n=>c(n.target.value),placeholder:"Find builds…","aria-label":"Find builds",className:"w-full h-8 pl-8 pr-2 text-xs"})]})}),e.jsxs("div",{role:"group","aria-label":"Builds",className:"flex-1 overflow-y-auto py-1",children:[e.jsxs(z,{active:a===null,onClick:()=>s(null),children:[e.jsxs("div",{className:"flex items-center gap-2 text-[13px] font-medium text-[var(--text)]",children:[e.jsx($e,{className:"h-3.5 w-3.5 text-[var(--text-dim)]","aria-hidden":"true"}),"All sessions"]}),e.jsxs("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)]",children:[E[r],", every build"]})]}),e.jsx("div",{className:"px-4 pt-3 pb-1 text-[11px] font-medium text-[var(--text-dim)]",children:"Builds"}),d.length===0&&e.jsx("div",{className:"px-4 py-6 text-xs text-[var(--text-dim)]",children:m}),d.map(n=>{const l=ue(n),j=l?U(n.sessionCount,"session"):`${q(n.createdAt)} · ${U(n.sessionCount,"session")}`;return e.jsxs(z,{active:n.id===a,onClick:()=>s(n.id),children:[e.jsx("div",{className:`text-[13px] font-medium truncate ${l?"text-[var(--text-muted)]":"text-[var(--text)]"}`,title:n.name||void 0,children:I(n)}),e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)] tabular-nums",children:j}),e.jsx(Ue,{build:n})]},n.id)})]})]})},Ge={neutral:"bg-[var(--text-dim)]",green:"bg-[var(--color-success)]",red:"bg-[var(--red)]",amber:"bg-[var(--amber)]",blue:"bg-[var(--blue)]"},B=({label:t,count:a,active:s,onClick:r,tone:x="neutral",bullet:c=!0})=>e.jsxs("button",{type:"button",onClick:r,"aria-pressed":s,className:`inline-flex items-center gap-2 h-8 px-3 rounded-md border text-[11px] font-semibold transition-colors cursor-pointer ${s?"bg-[var(--surface-2)] border-[var(--border-strong)] text-[var(--text)]":"bg-transparent border-transparent text-[var(--text-muted)] hover:text-[var(--text)] hover:bg-[var(--surface-2)]/50"}`,children:[c&&x!=="neutral"&&e.jsx("span",{className:`h-1.5 w-1.5 rounded-full ${Ge[x]}`}),e.jsx("span",{children:t}),typeof a=="number"&&e.jsx("span",{className:"font-mono text-[10px] text-[var(--text-dim)]",children:a})]}),He=({sessions:t,active:a,onChange:s,search:r,onSearchChange:x,totalMatching:c,totalUnfiltered:u,capped:o=!1})=>{const d=V(t);return e.jsxs("div",{className:"flex items-center gap-3 px-4 py-2.5 border-b border-[var(--border)] bg-[var(--surface)]",children:[e.jsxs("div",{role:"group","aria-label":"Status",className:"flex items-center gap-1.5",children:[e.jsx(B,{label:"All",count:d.all,active:a==="all",onClick:()=>s("all"),tone:"neutral",bullet:!1}),e.jsx(B,{label:"Passed",count:d.passed,active:a==="passed",onClick:()=>s("passed"),tone:"green"}),e.jsx(B,{label:"Failed",count:d.failed,active:a==="failed",onClick:()=>s("failed"),tone:"red"}),e.jsx(B,{label:"Running",count:d.running,active:a==="running",onClick:()=>s("running"),tone:"amber"})]}),e.jsxs("span",{className:"ml-auto text-[11px] text-[var(--text-dim)] tabular-nums whitespace-nowrap",children:[c," of ",u," sessions",o?" (the newest)":""]}),e.jsxs("div",{className:"relative w-64 shrink-0",children:[e.jsx(X,{className:"absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--text-dim)]"}),e.jsx(J,{type:"text",value:r,onChange:m=>x(m.target.value),placeholder:"Search tests, devices, people…","aria-label":"Search sessions",className:"w-full h-8 pl-8 pr-2 text-xs"})]})]})};function Xe(t){return(t.split("-")[0]||t).slice(0,8).toUpperCase()}const qe=({build:t,failedCount:a,selectedCount:s,onRetryFailed:r,onExport:x})=>{const c=a>0||s>0,[u,o]=M.useState(!1),d=M.useRef(null);M.useEffect(()=>{const n=l=>{d.current&&!d.current.contains(l.target)&&o(!1)};return document.addEventListener("mousedown",n),()=>document.removeEventListener("mousedown",n)},[]);const m=c?`Retry ${s||a} session${(s||a)===1?"":"s"}`:"No failed sessions to retry";return e.jsxs("header",{className:"flex items-start justify-between gap-4 px-4 py-3 border-b border-[var(--border)] bg-[var(--surface)]",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center gap-2 text-[11px] text-[var(--text-dim)]",children:[e.jsx("span",{children:"Build"}),e.jsxs("span",{className:"font-mono text-[var(--color-accent)]",title:t.id,children:["#",Xe(t.id)]}),e.jsx("span",{"aria-hidden":"true",children:"·"}),e.jsxs("span",{className:"tabular-nums",children:["Started ",q(t.createdAt)]})]}),e.jsx("h2",{className:"mt-0.5 text-sm font-semibold text-[var(--text)]",children:I(t)})]}),e.jsxs("div",{className:"flex items-center gap-2",ref:d,children:[e.jsxs(P,{type:"button",variant:"secondary",size:"sm",onClick:r,disabled:!c,title:m,className:"gap-1.5",children:[e.jsx(Ae,{className:"h-3.5 w-3.5"}),"Retry failed"]}),e.jsxs("div",{className:"relative",children:[e.jsxs(P,{type:"button",variant:"secondary",size:"sm",onClick:()=>o(n=>!n),className:"gap-1.5",children:[e.jsx(ke,{className:"h-3.5 w-3.5"}),"Export"]}),u&&e.jsxs("div",{className:"absolute top-full right-0 mt-1 w-40 rounded-md border border-[var(--border)] bg-[var(--surface)] shadow-lg overflow-hidden z-20",children:[e.jsx("button",{type:"button",onClick:()=>{o(!1),x("json")},className:"w-full text-left px-3 py-2 text-xs text-[var(--text)] hover:bg-[var(--surface-2)]",children:"Export as JSON"}),e.jsx("button",{type:"button",onClick:()=>{o(!1),x("csv")},className:"w-full text-left px-3 py-2 text-xs text-[var(--text)] hover:bg-[var(--surface-2)]",children:"Export as CSV"})]})]})]})]})};function Ve({platform:t}){const a=(t||"").toLowerCase(),s="h-3.5 w-3.5 shrink-0 text-[var(--text-dim)]";return a.includes("tv")?e.jsx(re,{className:s}):a.includes("pad")||a.includes("tablet")?e.jsx(Me,{className:s}):a.includes("android")||a.includes("ios")?e.jsx(ne,{className:s}):e.jsx(Ie,{className:s})}const We={passed:{label:"Passed",Icon:Ee,cls:"text-[var(--color-success)]"},failed:{label:"Failed",Icon:Te,cls:"text-[var(--color-danger)]"},running:{label:"Running",Icon:ae,cls:"text-[var(--color-warning)]"},other:{Icon:be,cls:"text-[var(--text-dim)]"}},Je="Unnamed session. Set xe:options.name in its capabilities to name it.",Ke=({session:t,buildName:a,showBuild:s,showSelection:r,selected:x,onToggleSelect:c,onOpen:u})=>{var N,C,$;const o=me(t.status),d=We[o],m=o==="failed",n=pe(t),l=m?(N=t.failure_reason)==null?void 0:N.trim():"",j=[he(t),t.device_version].filter(f=>f&&f!=="—").join(" "),S=[fe(t),j].filter(Boolean).join(" · "),v=[ve(t.ranOn),(C=t.owner)==null?void 0:C.name].filter(Boolean).join(" · "),p=m?"shadow-[inset_3px_0_0_var(--color-danger)]":"";return e.jsxs("tr",{onClick:u,tabIndex:0,onKeyDown:f=>{f.target!==f.currentTarget||f.key!=="Enter"&&f.key!==" "||(f.preventDefault(),u())},"data-outcome":o,className:`group border-b border-[var(--border)] cursor-pointer transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--color-focus-ring)] ${m?"bg-[rgb(var(--rgb-red)/0.04)] hover:bg-[rgb(var(--rgb-red)/0.08)]":"hover:bg-[var(--surface-2)]"}`,children:[r&&e.jsx("td",{className:`pl-4 pr-2 py-3 align-top ${p}`,onClick:f=>f.stopPropagation(),children:e.jsx("input",{type:"checkbox",checked:x,onChange:c,"aria-label":`Select session ${n.text}`})}),e.jsx("td",{className:`px-4 py-3 align-top ${r?"":p}`,children:e.jsxs("span",{className:`inline-flex items-center gap-1.5 text-xs font-medium ${d.cls}`,children:[e.jsx(d.Icon,{className:`h-3.5 w-3.5 shrink-0 ${o==="running"?"animate-spin":""}`,"aria-hidden":"true"}),d.label??Re(t.status||"unknown")]})}),e.jsxs("td",{className:"px-3 py-3 align-top min-w-0",children:[e.jsx("div",{className:`text-[13px] font-medium truncate ${n.source==="name"?"text-[var(--text)]":"text-[var(--text-muted)]"}`,title:n.source==="name"?n.text:`${n.text}
|
|
7
|
-
${Je}`,children:n.text}),l?e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--color-danger)] truncate",title:l,children:l}):s&&a&&e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)] truncate",title:a,children:a})]}),e.jsxs("td",{className:"px-3 py-3 align-top min-w-0",children:[e.jsxs("div",{className:"flex items-center gap-1.5 text-[13px] text-[var(--text)] min-w-0",children:[e.jsx(Ve,{platform:t.device_platform}),e.jsx("span",{className:"truncate",title:t.device_udid,children:S})]}),v&&e.jsx("div",{className:"mt-0.5 text-[11px] text-[var(--text-muted)] truncate",title:[t.ranOn==="here"?null:`Node ${t.node_id}`,($=t.owner)==null?void 0:$.email].filter(Boolean).join(`
|
|
8
|
-
`),children:v})]}),e.jsx("td",{className:"px-3 py-3 align-top text-xs text-[var(--text-muted)] tabular-nums whitespace-nowrap",title:je(t.startTime),children:ge(t.startTime)}),e.jsx("td",{className:"px-3 py-3 align-top text-xs text-[var(--text)] tabular-nums whitespace-nowrap text-right",children:D(Ne(t))}),e.jsx("td",{className:"pr-4 pl-1 py-3 align-top text-right",children:e.jsx(Be,{className:"inline h-4 w-4 text-[var(--text-dim)] group-hover:text-[var(--text)]","aria-hidden":"true"})})]})},T="px-3 py-2 text-[11px] font-medium text-[var(--text-dim)]",G=({sessions:t,statusFilter:a,searchQuery:s,buildNames:r,showBuild:x,showSelection:c,selectedIds:u,onToggleSelect:o,onToggleSelectAll:d,onOpenRow:m,empty:n})=>{const l=h.useMemo(()=>W(t,a,s),[t,a,s]);if(t.length===0)return e.jsx(e.Fragment,{children:n});if(l.length===0)return e.jsx("div",{className:"px-6 py-12 text-center text-xs text-[var(--text-dim)]",children:"No sessions match the current filters."});const j=l.every(p=>u.has(p.id)),S=l.some(p=>u.has(p.id)),v=l.some(p=>{var N;return!((N=p.name)!=null&&N.trim())});return e.jsxs("div",{className:"flex-1 overflow-y-auto",children:[e.jsxs("table",{className:"w-full text-left table-fixed",children:[e.jsx("thead",{className:"sticky top-0 bg-[var(--surface)] border-b border-[var(--border)] z-10",children:e.jsxs("tr",{children:[c&&e.jsx("th",{className:"pl-4 pr-2 py-2 w-[44px]",children:e.jsx("input",{type:"checkbox","aria-label":"Select all shown",checked:j,ref:p=>{p&&(p.indeterminate=S&&!j)},onChange:()=>d(l.map(p=>p.id))})}),e.jsx("th",{className:`${T} pl-4 w-[112px]`,children:"Status"}),e.jsx("th",{className:T,children:"Test"}),e.jsx("th",{className:`${T} w-[240px]`,children:"Device"}),e.jsx("th",{className:`${T} w-[124px]`,children:"Started"}),e.jsx("th",{className:`${T} w-[84px] text-right`,children:"Duration"}),e.jsx("th",{className:"w-[40px]",children:e.jsx("span",{className:"sr-only",children:"Open"})})]})}),e.jsx("tbody",{children:l.map(p=>e.jsx(Ke,{session:p,buildName:p.build_id?r.get(p.build_id):null,showBuild:x,showSelection:c,selected:u.has(p.id),onToggleSelect:()=>o(p.id),onOpen:()=>m(p)},p.id))})]}),v&&e.jsxs("p",{className:"px-4 py-3 text-[11px] text-[var(--text-dim)]",children:["Unnamed sessions show their app. Name a test with"," ",e.jsx("code",{className:"font-mono text-[var(--text-muted)]",children:"xe:options.name"})," and a run with"," ",e.jsx("code",{className:"font-mono text-[var(--text-muted)]",children:"xe:options.build"})," in its capabilities."]})]})};function Qe(t,a){const s=Math.round(t);return s===0?{text:`No change vs prior ${a}`,tone:"neutral"}:s>0?{text:`+${s} pts vs prior ${a}`,tone:"good"}:{text:`−${-s} pts vs prior ${a}`,tone:"bad"}}const Ye=({summary:t,periodLabel:a})=>{if(!t)return e.jsxs("div",{className:"grid grid-cols-4 gap-3 px-6 py-3 border-b border-[var(--border)]",children:[e.jsx(w,{label:"Pass rate",value:"—"}),e.jsx(w,{label:"Failed",value:"—"}),e.jsx(w,{label:"Running now",value:"—"}),e.jsx(w,{label:"Median duration",value:"—"})]});const{current:s,previous:r,runningNow:x}=t,c=ye(s),u=s.passed+s.failed,o=a?we(s,r):null,d=o!==null?Qe(o,a):u>0?{text:`${s.passed} of ${u} passed`,tone:"neutral"}:{text:"No finished sessions",tone:"neutral"};return e.jsxs("div",{className:"grid grid-cols-4 gap-3 px-6 py-3 border-b border-[var(--border)]",children:[e.jsx(w,{label:"Pass rate",value:c===null?"—":`${Math.round(c)}%`,note:d.text,noteTone:d.tone}),e.jsx(w,{label:"Failed",value:s.failed,valueCls:s.failed>0?"text-[var(--color-danger)]":"text-[var(--text)]",note:`of ${s.total} session${s.total===1?"":"s"}`}),e.jsx(w,{label:"Running now",value:x.sessions,note:x.sessions>0?`on ${x.devices} device${x.devices===1?"":"s"}`:"Nothing running"}),e.jsx(w,{label:"Median duration",value:D(s.medianMs),note:s.p90Ms===null?"No finished sessions":`p90 ${D(s.p90Ms)}`})]})},Ze={"24h":"24 hours","7d":"7 days","30d":"30 days"},et=`"xe:options": {
|
|
9
|
-
"name": "Checkout happy path",
|
|
10
|
-
"build": "Nightly smoke"
|
|
11
|
-
}`,tt=()=>e.jsxs("div",{className:"text-xs text-[var(--text-muted)]",children:[e.jsx("p",{children:"Name each test, and group a run, in its capabilities:"}),e.jsx("pre",{className:"mt-2 inline-block rounded-md border border-[var(--border)] bg-[var(--surface-2)] px-3 py-2 text-left font-mono text-[11px] text-[var(--text)]",children:et})]}),jt=()=>{const t=le(),{buildId:a}=oe(),{toast:s}=ie(),[r,x]=h.useState("7d"),[c,u]=h.useState("all"),[o,d]=h.useState(""),[m,n]=h.useState(new Set),l=Se({withSessions:!0,timeFilter:r}),j=Fe(r,l.selectedBuildId),{selectBuild:S,selectedBuildId:v}=l;M.useEffect(()=>{const i=a??null;i!==v&&(S(i),n(new Set))},[a,v,S]);const p=i=>t(i?`/builds/${i}`:"/builds"),N=i=>t(`/builds/${encodeURIComponent(i.build_id||"none")}/sessions/${i.id}`),C=h.useCallback(i=>{n(b=>{const g=new Set(b);return g.has(i)?g.delete(i):g.add(i),g})},[]),$=h.useCallback(i=>{n(b=>{const g=i.length>0&&i.every(y=>b.has(y)),k=new Set(b);return g?i.forEach(y=>k.delete(y)):i.forEach(y=>k.add(y)),k})},[]),f=h.useMemo(()=>new Map(l.builds.map(i=>[i.id,I(i)])),[l.builds]),L=l.builds.find(i=>i.id===v)||null,K=V(l.sessions),Q=()=>{s("Bulk retry lands in a future release.","info")},Y=async i=>{if(v)try{const b=m.size>0?Array.from(m):void 0,{blob:g,filename:k}=await H.exportBuild(v,i,b),y=URL.createObjectURL(g),R=document.createElement("a");R.href=y,R.download=k,document.body.appendChild(R),R.click(),document.body.removeChild(R),URL.revokeObjectURL(y);const A=b?b.length:"all";s(`Exported ${A} session${A===1?"":"s"} as ${i.toUpperCase()}.`,"success")}catch(b){const g=b instanceof Error?b.message:String(b);s(`Export failed: ${g}`,"error")}},_=v===null,Z=_&&r!=="all"?Ze[r]:null,ee=!_&&!L&&!l.loading,O=e.jsx(He,{sessions:l.sessions,active:c,onChange:u,search:o,onSearchChange:d,totalMatching:W(l.sessions,c,o).length,totalUnfiltered:l.sessions.length,capped:l.sessions.length>=Ce});return e.jsxs("div",{className:"flex flex-col h-full",children:[e.jsx(Le,{icon:ce,title:"Sessions",subtitle:"Every test session, newest first, with its build, device and outcome.",action:e.jsx(_e,{value:r,onChange:i=>x(i.target.value),selectSize:"sm","aria-label":"Period",className:"w-40 text-xs",children:Object.keys(E).map(i=>e.jsx("option",{value:i,children:E[i]},i))})}),e.jsx(Ye,{summary:j,periodLabel:Z}),e.jsxs("div",{className:"flex flex-1 min-h-0",children:[e.jsx(ze,{builds:l.builds,selectedBuildId:v,onSelect:p,timeFilter:r}),e.jsx("section",{className:"flex-1 flex flex-col min-w-0",children:ee?e.jsx(F,{title:"This build isn’t available",description:"It may have been removed, or belong to a team you’re not on."}):_?e.jsxs(e.Fragment,{children:[e.jsx("h2",{className:"sr-only",children:"All sessions"}),O,e.jsx(G,{sessions:l.sessions,statusFilter:c,searchQuery:o,buildNames:f,showBuild:!0,showSelection:!1,selectedIds:m,onToggleSelect:C,onToggleSelectAll:$,onOpenRow:N,empty:l.loading?null:e.jsx("div",{className:"px-6 py-6",children:e.jsx(F,{icon:e.jsx(De,{className:"h-5 w-5"}),title:r==="all"?"No sessions yet":`No sessions in the ${E[r].toLowerCase()}`,description:"Sessions appear here as your tests run.",action:e.jsx(tt,{})})})})]}):L?e.jsxs(e.Fragment,{children:[e.jsx(qe,{build:L,failedCount:K.failed,selectedCount:m.size,onRetryFailed:Q,onExport:Y}),O,e.jsx(G,{sessions:l.sessions,statusFilter:c,searchQuery:o,buildNames:f,showBuild:!1,showSelection:!0,selectedIds:m,onToggleSelect:C,onToggleSelectAll:$,onOpenRow:N,empty:e.jsx("div",{className:"px-6 py-12 text-center text-xs text-[var(--text-dim)]",children:"No sessions in this build yet. Trigger one from your test runner."})})]}):null})]})]})};export{jt as BuildsPage,jt as default};
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import{c as k,r as i,p as I,X as b,j as p}from"./index-tW_VASh2.js";import{f as A}from"./time-B8FIgwPE.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 E=[["path",{d:"M10.1 2.182a10 10 0 0 1 3.8 0",key:"5ilxe3"}],["path",{d:"M13.9 21.818a10 10 0 0 1-3.8 0",key:"11zvb9"}],["path",{d:"M17.609 3.721a10 10 0 0 1 2.69 2.7",key:"1iw5b2"}],["path",{d:"M2.182 13.9a10 10 0 0 1 0-3.8",key:"c0bmvh"}],["path",{d:"M20.279 17.609a10 10 0 0 1-2.7 2.69",key:"1ruxm7"}],["path",{d:"M21.818 10.1a10 10 0 0 1 0 3.8",key:"qkgqxc"}],["path",{d:"M3.721 6.391a10 10 0 0 1 2.7-2.69",key:"1mcia2"}],["path",{d:"M6.391 20.279a10 10 0 0 1-2.69-2.7",key:"1fvljs"}]],U=k("circle-dashed",E);function D(e){switch(e){case"success":case"passed":case"ended":return"passed";case"failed":case"error":case"timeout":return"failed";case"running":return"running";default:return"other"}}function Y(e){const t={all:e.length,passed:0,failed:0,running:0};for(const n of e){const r=D(n.status);r!=="other"&&(t[r]+=1)}return t}function F(e,t){return t==="all"?!0:D(e.status)===t}function X(e,t,n){const r=n.trim().toLowerCase();return e.filter(s=>{var o,d;return!(!F(s,t)||r&&![s.id,C(s).text,s.name,s.failure_reason,s.device_name,s.device_platform,s.device_version,s.node_id,s.ranOn,(o=s.owner)==null?void 0:o.name,(d=s.owner)==null?void 0:d.email].filter(Boolean).join(" ").toLowerCase().includes(r))})}function z(e){var n;const t=(n=e.device_name)==null?void 0:n.trim();return t&&t.length>0?t:"Unknown Device"}function V(e){const t=e.device_platform??"";if(!t)return"—";const n=t.toLowerCase();return n==="ios"?"iOS":n==="tvos"?"tvOS":n.charAt(0).toUpperCase()+n.slice(1)}function G(e){return A(e)}function K(e){if(!e.startTime)return null;const t=Date.parse(e.startTime);if(!Number.isFinite(t))return null;const n=e.endTime?Date.parse(e.endTime):Date.now();return Number.isFinite(n)?Math.max(0,n-t):null}function Q(e){if(e==null||!Number.isFinite(e)||e<0)return"—";const t=e/1e3,n=Math.floor(t/3600),r=Math.floor(t%3600/60),o=(t-n*3600-r*60).toFixed(1);return n>0?`${n}h ${r}m ${o}s`:r>0?`${r}m ${o}s`:`${o}s`}const N=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],g=e=>String(e).padStart(2,"0");function h(e,t){const n=e[`appium:${t}`]??e[t];return typeof n=="string"&&n.trim()?n.trim():null}function B(e){if(!e)return null;let t;try{t=JSON.parse(e)}catch{return null}if(!t||typeof t!="object")return null;let n=t;if(n.capabilities&&typeof n.capabilities=="object"&&(n=n.capabilities),n.alwaysMatch||n.firstMatch){const r=Array.isArray(n.firstMatch)?n.firstMatch[0]:void 0;return{...n.alwaysMatch||{},...r||{}}}return n}function x(e){const t=B(e);if(!t)return null;const n=h(t,"appPackage")??h(t,"bundleId");if(n)return n;const r=h(t,"app");if(r){const s=r.split(/[?#]/)[0].split(/[\\/]/).pop()??"";if(s&&s!=="download")return s}return h(t,"browserName")}function C(e){var r;const t=(r=e.name)==null?void 0:r.trim();if(t)return{text:t,source:"name"};const n=x(e.session_capabilities)??x(e.desired_capabilities);return n?{text:n,source:"app"}:{text:`Session ${e.id.slice(0,8)}`,source:"id"}}function S(e){return`${N[e.getMonth()]} ${e.getDate()}, ${g(e.getHours())}:${g(e.getMinutes())}`}function W(e){const t=e?new Date(e):null;return t&&!Number.isNaN(t.getTime())?S(t):"—"}function j(e){var n;const t=(n=e.name)==null?void 0:n.trim();return!t||t==="Default Build"}function Z(e){if(!j(e))return e.name.trim();const t=new Date(e.createdAt);return Number.isNaN(t.getTime())?"Build":`Build · ${S(t)}`}const ee={all:"All time","24h":"Last 24 hours","7d":"Last 7 days","30d":"Last 30 days"},O={"24h":864e5,"7d":7*864e5,"30d":30*864e5};function R(e,t){return e==="all"?null:new Date(t-O[e]).toISOString()}function te(e){if(e==null||!Number.isFinite(e)||e<0)return"—";if(e<1e3)return"<1s";const t=Math.floor(e/1e3),n=Math.floor(t/3600),r=Math.floor(t%3600/60),s=t%60;return n>0?`${n}h ${r}m`:r>0?`${r}m ${s}s`:`${s}s`}function ne(e,t=new Date){if(!e)return"—";const n=new Date(e);if(Number.isNaN(n.getTime()))return"—";const r=`${g(n.getHours())}:${g(n.getMinutes())}`;return n.getFullYear()!==t.getFullYear()?`${N[n.getMonth()]} ${n.getDate()}, ${n.getFullYear()} ${r}`:n.getMonth()===t.getMonth()&&n.getDate()===t.getDate()?r:S(n)}function y(e){const t=e.passed+e.failed;return t>0?e.passed/t*100:null}function re(e,t){const n=y(e),r=t?y(t):null;return n===null||r===null?null:n-r}function se(e){return e?e==="here"?"This server":e:null}const P=3e3,ae=500;function ie(e={}){const{withSessions:t=!1,timeFilter:n="all"}=e,[r,s]=i.useState([]),[o,d]=i.useState([]),[c,$]=i.useState(null),[w,_]=i.useState(!0),[T,v]=i.useState(null),l=i.useRef(!0),{on:f}=I(),M=i.useCallback(()=>{if(!t)return Promise.resolve([]);if(c)return b.getSessions({buildId:c});const a=R(n,Date.now())??void 0;return b.getSessions({since:a})},[t,c,n]),u=i.useCallback(async()=>{try{const[a,m]=await Promise.all([b.getBuilds(),M()]);if(!l.current)return;s(Array.isArray(a)?a:[]),d(Array.isArray(m)?m:[]),v(null)}catch(a){if(!l.current)return;v(a instanceof Error?a.message:String(a))}finally{l.current&&_(!1)}},[M]);return i.useEffect(()=>{l.current=!0,u();const a=setInterval(u,P);return()=>{l.current=!1,clearInterval(a)}},[u]),i.useEffect(()=>{const a=()=>u(),m=[f("session_started",a),f("session_stopped",a),f("device_added",a),f("device_removed",a)];return()=>{for(const L of m)L()}},[f,u]),{builds:r,selectedBuildId:c,sessions:o,loading:w,error:T,selectBuild:$,refresh:u}}const q={neutral:"text-[var(--text-muted)]",good:"text-[var(--color-success)]",bad:"text-[var(--color-danger)]"},oe=({label:e,value:t,valueCls:n="text-[var(--text)]",note:r,noteTone:s="neutral"})=>p.jsxs("div",{className:"min-w-0 rounded-lg border border-[var(--border)] bg-[var(--surface)] px-4 py-3",children:[p.jsx("div",{className:"text-xs text-[var(--text-muted)]",children:e}),p.jsx("div",{className:`mt-1 text-2xl font-semibold tabular-nums leading-tight truncate ${n}`,children:t}),p.jsx("div",{className:`mt-0.5 text-[11px] truncate ${q[s]}`,children:r??" "})]});export{U as C,O as P,oe as S,ee as T,Y as a,Z as b,D as c,C as d,z as e,W as f,ne as g,G as h,j as i,te as j,K as k,X as l,y as m,re as n,ae as o,V as p,Q as q,se as r,R as s,ie as u};
|