@xenon-device-management/xenon 1.3.0 → 1.4.0
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/README.md +17 -0
- package/lib/package.json +1 -1
- package/lib/public/assets/{Layouts-D6IPfwoe.js → Layouts-BV_fQEKa.js} +1 -1
- package/lib/public/assets/{ai-settings-CflyFKan.js → ai-settings-BnoeJplt.js} +2 -2
- package/lib/public/assets/api-keys-TLWTJYrE.js +1 -0
- package/lib/public/assets/{apps-Da4dvQ1J.js → apps-BGCcB8_p.js} +2 -2
- package/lib/public/assets/arrow-left-B6-SH02F.js +6 -0
- package/lib/public/assets/{badge-BNR9umdu.js → badge-BtQKmQsj.js} +1 -1
- package/lib/public/assets/button-CNLKazSa.js +16 -0
- package/lib/public/assets/{calendar-fehdBtun.js → calendar-CPNkf0zl.js} +1 -1
- package/lib/public/assets/{clock-DrpxSvCL.js → clock-C8ZaLIQA.js} +1 -1
- package/lib/public/assets/copy-DCAmOyY9.js +11 -0
- package/lib/public/assets/{cpu-tuyMVZ4I.js → cpu-D3PzrmhN.js} +1 -1
- package/lib/public/assets/{device-explorer-DOfRH3zm.js → device-explorer-BfFe3bRe.js} +36 -41
- package/lib/public/assets/{index-BaTiUCeH.js → index-Do3u3FMx.js} +38 -28
- package/lib/public/assets/{lock-C6CoqSr2.js → lock-DNzAbx6Q.js} +1 -1
- package/lib/public/assets/{maintenance-settings-CM2oC7-i.js → maintenance-settings-yHAKnfz_.js} +2 -2
- package/lib/public/assets/{mouse-pointer-2-CXdnjXIg.js → mouse-pointer-2-B0KBsfLG.js} +1 -1
- package/lib/public/assets/{plus-B4B1Hukt.js → plus-DecbehZh.js} +1 -1
- package/lib/public/assets/{session-dashboard-B5OPMTz5.js → session-dashboard-CJvDLc6D.js} +17 -22
- package/lib/public/assets/settings-DjTQTjSq.js +1 -0
- package/lib/public/assets/shield-alert-Br1Ebl1G.js +6 -0
- package/lib/public/assets/teams-LILlQka3.js +1 -0
- package/lib/public/assets/{trash-2-NJMZJ2Ol.js → trash-2-DEafUZlN.js} +1 -1
- package/lib/public/assets/{useSocket-Ct2wo7P2.js → useSocket-Dzv_TTsO.js} +1 -1
- package/lib/public/assets/{webhook-settings-Cz35-QJ7.js → webhook-settings-CVwvtwI7.js} +1 -1
- package/lib/public/assets/{zap-CssSMAN5.js → zap-DCsIYdwr.js} +1 -1
- package/lib/public/index.html +1 -1
- package/lib/src/XenonCapabilityManager.js +30 -0
- package/lib/src/app/index.js +3 -0
- package/lib/src/app/routers/apikeys.js +5 -2
- package/lib/src/app/routers/grid.js +46 -2
- package/lib/src/app/routers/teams.js +66 -0
- package/lib/src/dashboard/event-manager.js +2 -0
- package/lib/src/data-service/device-store.js +12 -0
- package/lib/src/data-service/prisma-store.js +7 -0
- package/lib/src/device-utils.js +7 -1
- package/lib/src/generated/client/edge.js +18 -7
- package/lib/src/generated/client/index-browser.js +15 -4
- package/lib/src/generated/client/index.d.ts +2613 -391
- package/lib/src/generated/client/index.js +18 -7
- package/lib/src/generated/client/package.json +1 -1
- package/lib/src/generated/client/schema.prisma +18 -0
- package/lib/src/generated/client/wasm.js +15 -4
- package/lib/src/logger.js +17 -0
- package/lib/src/middleware/apiKeyMiddleware.js +22 -2
- package/lib/src/services/ApiKeyService.js +20 -1
- package/lib/src/services/ServerManager.js +10 -3
- package/lib/src/services/SessionLifecycleService.js +53 -4
- package/lib/src/services/TeamService.js +100 -0
- package/lib/src/sessions/XenonSession.js +3 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260424033547_add_teams/migration.sql +101 -0
- package/prisma/schema.prisma +18 -0
- package/lib/public/assets/button-hZFV1ypT.js +0 -26
- package/lib/public/assets/settings-BTHP7fj3.js +0 -1
- /package/lib/public/assets/{Layouts-DPMls9vh.css → settings-DPMls9vh.css} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{r as i,j as e,g as j,l as S,B as C,i as y,T as N}from"./index-Do3u3FMx.js";import{X as c}from"./index-C1DBaoSh.js";/* empty css */import{A as b}from"./Layouts-BV_fQEKa.js";import{C as k}from"./clock-C8ZaLIQA.js";import{C as I}from"./calendar-CPNkf0zl.js";import{M}from"./mouse-pointer-2-B0KBsfLG.js";const F=()=>{const[a,t]=i.useState({healthCheckIntervalMs:3e4,healthCheckSchedule:"",enableSelfHealing:!0}),[g,d]=i.useState(!0),[m,h]=i.useState(!1),[l,n]=i.useState(null);i.useEffect(()=>{o()},[]);const o=async()=>{d(!0);try{const s=await c.getGlobalConfig();t({healthCheckIntervalMs:s.healthCheckIntervalMs||3e4,healthCheckSchedule:s.healthCheckSchedule||"",enableSelfHealing:s.enableSelfHealing!==void 0?s.enableSelfHealing:!0})}catch(s){console.error("Failed to load settings",s),n({type:"error",message:"Failed to access infrastructure parameters."})}finally{d(!1)}},u=async(s=a)=>{h(!0),n(null);try{await c.updateGlobalConfig(s),n({type:"success",message:"Infrastructure parameters synchronized across fleet."}),setTimeout(()=>n(null),5e3)}catch(r){console.error("Failed to save settings",r),n({type:"error",message:"Synchronization failed. Check network integrity."})}finally{h(!1)}},v=async()=>{const s={healthCheckIntervalMs:3e4,healthCheckSchedule:"",enableSelfHealing:!0};t(s),await u(s);try{await c.resetMetrics()}catch(r){console.error("Failed to reset metrics",r)}},p=[{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 * * * *"},{label:"Disable Schedule",value:""}],f=s=>s?s.split(" ").filter(x=>x!=="").length!==5?"Invalid Cron format (needs 5 parts)":s==="0 2 * * *"?"Quiet hours: Daily at 2:00 AM":s==="0 * * * *"?"Standard rotation: Start of every hour":s==="*/30 * * * *"?"Balanced: Every 30 minutes":s==="*/10 * * * *"?"Intensive: Every 10 minutes":`Custom orchestration: ${s}`:"Using Idle Health Frequency (Continuous/Passive mode).";return g?e.jsxs("div",{className:"settings-loading",children:[e.jsx(j,{className:"animate-spin",size:32}),e.jsx("span",{children:"Synchronizing Global Infrastructure..."})]}):e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsx("div",{className:"scanline",style:{position:"absolute",inset:0,pointerEvents:"none",opacity:.05,zIndex:1001}}),e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{className:"settings-title-group",children:[e.jsx(S,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:"Infrastructure Control"})]}),e.jsx("p",{className:"settings-subtitle",children:"Manage core farm parameters, heartbeat frequency, and maintenance orchestrations across the global registry."})]}),e.jsxs("div",{className:"settings-content",children:[e.jsxs("div",{className:"settings-grid",children:[e.jsxs("div",{className:"setting-card stagger-1",children:[e.jsxs("div",{className:"setting-card-header",children:[e.jsx(k,{size:16}),e.jsx("h4",{children:"Idle Health Frequency"})]}),e.jsx("p",{className:"section-description-dense",children:"Frequency of passive health pings when the system is in idle state."}),e.jsx("div",{className:"setting-field",children:e.jsxs("div",{className:"input-group",children:[e.jsx("input",{type:"number",value:a.healthCheckIntervalMs,onChange:s=>t({...a,healthCheckIntervalMs:parseInt(s.target.value)}),min:5e3,step:5e3}),e.jsx("span",{className:"code-font",children:"MS"})]})}),e.jsx("div",{className:"setting-hint-clean",children:"Minimum safe value: 5000ms. Note: This frequency is overridden when a schedule is active."})]}),e.jsxs("div",{className:"setting-card stagger-2",children:[e.jsxs("div",{className:"setting-card-header",children:[e.jsx(I,{size:16}),e.jsx("h4",{children:"Deep Diagnostic Schedule"})]}),e.jsx("p",{className:"section-description-dense",children:"Execute intensive reliability bursts (WDA restarts, Cache purges) using standardized Cron syntax."}),e.jsx("div",{className:"setting-field",children:e.jsx("div",{className:"setting-input-wrapper",children:e.jsx("input",{type:"text",placeholder:"e.g. 0 * * * * (At internal min 0)",value:a.healthCheckSchedule,onChange:s=>t({...a,healthCheckSchedule:s.target.value})})})}),e.jsxs("div",{className:"cron-preview",children:[e.jsx("span",{className:"preview-label",children:"Active Logic:"}),e.jsx("span",{className:"preview-value",children:f(a.healthCheckSchedule)})]}),e.jsxs("div",{className:"cron-presets",children:[e.jsxs("div",{className:"presets-label",children:[e.jsx(M,{size:12}),e.jsx("span",{children:"Intent-Based Presets:"})]}),e.jsx("div",{className:"presets-grid",children:p.map(s=>e.jsx("button",{className:`preset-chip ${a.healthCheckSchedule===s.value?"active":""}`,onClick:()=>t({...a,healthCheckSchedule:s.value}),children:s.label},s.label))})]})]}),e.jsxs("div",{className:"setting-card stagger-3",children:[e.jsxs("div",{className:"setting-card-header",children:[e.jsx(C,{size:16}),e.jsx("h4",{children:"AI Self-Healing"})]}),e.jsx("p",{className:"section-description-dense",children:"Automatically intercept and recover from failing locators using Xenon's 5-tier strategy."}),e.jsxs("div",{className:"toggle-group",children:[e.jsxs("label",{className:"switch",children:[e.jsx("input",{type:"checkbox",checked:a.enableSelfHealing,onChange:s=>t({...a,enableSelfHealing:s.target.checked})}),e.jsx("span",{className:"slider round"})]}),e.jsx("span",{className:"toggle-label",children:a.enableSelfHealing?"ENABLED":"DISABLED"})]}),e.jsx("div",{className:"setting-hint-clean",children:"When enabled, Xenon will attempt to find elements via Fuzzy XML, OCR, Visual AI, and LLM before failing a test."})]})]}),l&&e.jsxs("div",{className:`status-banner ${l.type}`,children:[l.type==="success"?e.jsx(y,{size:18}):e.jsx(N,{size:18}),e.jsx("span",{children:l.message})]})]}),e.jsx(b,{onSave:u,onDiscard:o,onRestoreDefaults:v,isSaving:m,saveLabel:"Save Configuration"})]})};export{F as Settings};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{c as e}from"./index-Do3u3FMx.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 a=[["path",{d:"M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z",key:"oel41y"}],["path",{d:"M12 8v4",key:"1got3b"}],["path",{d:"M12 16h.01",key:"1drbdi"}]],o=e("shield-alert",a);export{o as S};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{u as C,r as a,j as e,g as T,U as I,T as L,S as O,X as K}from"./index-Do3u3FMx.js";/* empty css */import{S as F}from"./shield-alert-Br1Ebl1G.js";import{P as S}from"./plus-DecbehZh.js";import{A as J}from"./arrow-left-B6-SH02F.js";import{T as B}from"./trash-2-DEafUZlN.js";async function m(i,o){const t=await fetch(i,{credentials:"include",...o});if(!t.ok){const n=await t.json().catch(()=>({}));throw Object.assign(new Error(n.error||`HTTP ${t.status}`),{status:t.status})}return t.json()}const se=()=>{const{toast:i}=C(),[o,t]=a.useState([]),[n,h]=a.useState(!0),[c,x]=a.useState(!1),[g,y]=a.useState(!1),[b,f]=a.useState(null),j=async()=>{h(!0);try{const l=await m("/xenon/api/teams");t(l),x(!1)}catch(l){l.status===403?x(!0):i(`Failed to load teams: ${l.message}`,"error")}finally{h(!1)}};return a.useEffect(()=>{j()},[]),n?e.jsxs("div",{className:"settings-loading",children:[e.jsx(T,{className:"animate-spin",size:32}),e.jsx("span",{children:"Loading teams…"})]}):c?e.jsx("div",{className:"settings-container mesh-gradient-infra",children:e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{className:"settings-title-group",children:[e.jsx(F,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:"Teams"})]}),e.jsxs("p",{className:"settings-subtitle",children:["Your key lacks the ",e.jsx("code",{children:"admin"})," scope. Ask an administrator to promote your key before you can manage teams."]})]})}):b?e.jsx(X,{team:b,onBack:()=>{f(null),j()}}):e.jsxs("div",{className:"settings-container mesh-gradient-infra",children:[e.jsxs("div",{className:"settings-header",children:[e.jsxs("div",{className:"settings-title-group",children:[e.jsx(I,{className:"settings-icon infra-icon",size:28}),e.jsx("h2",{children:"Teams"})]}),e.jsx("p",{className:"settings-subtitle",children:"Group devices and API keys by team. Devices with no team assignment stay in the shared pool — visible to every authenticated key."})]}),e.jsxs("div",{className:"settings-content",children:[e.jsx("div",{style:{display:"flex",justifyContent:"flex-end",marginBottom:16},children:e.jsxs("button",{className:"save-btn",onClick:()=>y(!0),children:[e.jsx(S,{size:18}),"New team"]})}),o.length===0?e.jsxs("div",{className:"setting-card",style:{textAlign:"center",padding:48},children:[e.jsx(L,{size:32,style:{opacity:.4}}),e.jsx("p",{style:{marginTop:12},children:"No teams yet. Every device is in the shared pool until you create a team and assign it."})]}):e.jsx("div",{className:"setting-card",style:{padding:0,overflowX:"auto"},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid rgba(255,255,255,0.08)"},children:[e.jsx("th",{style:r,children:"Name"}),e.jsx("th",{style:r,children:"Devices"}),e.jsx("th",{style:r,children:"Members"}),e.jsx("th",{style:r,children:"Created"}),e.jsx("th",{style:{...r,textAlign:"right"},children:"Actions"})]})}),e.jsx("tbody",{children:o.map(l=>e.jsxs("tr",{style:{borderBottom:"1px solid rgba(255,255,255,0.05)",cursor:"pointer"},onClick:()=>f(l),children:[e.jsxs("td",{style:d,children:[e.jsx("strong",{children:l.name}),e.jsx("div",{style:{fontSize:"0.75em",opacity:.5,fontFamily:"monospace"},children:l.id.slice(0,8)})]}),e.jsx("td",{style:d,children:l.deviceCount}),e.jsx("td",{style:d,children:l.memberCount}),e.jsx("td",{style:d,children:new Date(l.createdAt).toLocaleDateString()}),e.jsx("td",{style:{...d,textAlign:"right"},children:e.jsx(W,{team:l,onDeleted:j})})]},l.id))})]})})]}),g&&e.jsx(q,{onClose:()=>y(!1),onCreated:()=>{y(!1),j()}})]})},q=({onClose:i,onCreated:o})=>{const{toast:t}=C(),[n,h]=a.useState(""),[c,x]=a.useState(!1),g=async()=>{if(!n.trim())return t("Name required","error");x(!0);try{await m("/xenon/api/teams",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n.trim()})}),t("Team created","success"),o()}catch(y){t(y.message,"error")}finally{x(!1)}};return e.jsx(G,{onClose:i,title:"New team",children:e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:16},children:[e.jsxs("label",{children:[e.jsx("div",{style:H,children:"Name"}),e.jsx("input",{type:"text",value:n,onChange:y=>h(y.target.value),placeholder:"android-team, qa-ios, …",style:A,autoFocus:!0})]}),e.jsxs("div",{style:{display:"flex",justifyContent:"flex-end",gap:8},children:[e.jsx("button",{className:"reset-btn",onClick:i,disabled:c,children:"Cancel"}),e.jsxs("button",{className:"save-btn",onClick:g,disabled:c,children:[c?e.jsx(T,{className:"animate-spin",size:18}):e.jsx(S,{size:18}),c?"Creating…":"Create"]})]})]})})},W=({team:i,onDeleted:o})=>{const{toast:t}=C(),[n,h]=a.useState(!1),c=async x=>{if(x.stopPropagation(),!!window.confirm(`Delete team "${i.name}"? It must have no devices and no members.`)){h(!0);try{await m(`/xenon/api/teams/${i.id}`,{method:"DELETE"}),t("Team deleted","success"),o()}catch(g){t(g.message,"error")}finally{h(!1)}}};return e.jsxs("button",{className:"reset-btn",disabled:n,onClick:c,style:{color:"var(--accent-red)"},children:[e.jsx(B,{size:14}),n?"Deleting…":"Delete"]})},X=({team:i,onBack:o})=>{const{toast:t}=C(),[n,h]=a.useState([]),[c,x]=a.useState([]),[g,y]=a.useState([]),[b,f]=a.useState([]),[j,l]=a.useState(!0),[v,k]=a.useState(""),[N,D]=a.useState(""),u=async()=>{l(!0);try{const[s,p,U]=await Promise.all([m(`/xenon/api/teams/${i.id}/members`),m("/xenon/api/apikeys"),m("/xenon/api/device")]);h(s),x(p);const z=U;y(z.filter(w=>w.teamId===i.id)),f(z.filter(w=>!w.teamId))}catch(s){t(`Failed to load team detail: ${s.message}`,"error")}finally{l(!1)}};a.useEffect(()=>{u()},[i.id]);const P=async()=>{if(v)try{await m(`/xenon/api/teams/${i.id}/members`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({apiKeyId:v,role:"member"})}),t("Member added","success"),k(""),u()}catch(s){t(s.message,"error")}},E=async s=>{if(window.confirm("Remove this key from the team?"))try{await m(`/xenon/api/teams/${i.id}/members/${s}`,{method:"DELETE"}),t("Member removed","success"),u()}catch(p){t(p.message,"error")}},R=async()=>{if(N)try{await m(`/xenon/api/device/${encodeURIComponent(N)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:i.id})}),t("Device assigned","success"),D(""),u()}catch(s){t(s.message,"error")}},$=async s=>{if(window.confirm("Return this device to the shared pool?"))try{await m(`/xenon/api/device/${encodeURIComponent(s)}/team`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({teamId:null})}),t("Device unassigned","success"),u()}catch(p){t(p.message,"error")}},M=c.filter(s=>!n.some(p=>p.id===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:o,className:"reset-btn",style:{padding:"4px 8px"},"aria-label":"Back",children:e.jsx(J,{size:18})}),e.jsxs("div",{className:"settings-title-group",children:[e.jsx(I,{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."'})]}),j?e.jsx("div",{className:"settings-loading",children:e.jsx(T,{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 (",n.length,")"]}),e.jsxs("div",{className:"setting-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs("select",{value:v,onChange:s=>k(s.target.value),style:{...A,flex:1},children:[e.jsx("option",{value:"",children:"Select an API key to add…"}),M.map(s=>e.jsxs("option",{value:s.id,children:[s.name," (",s.scopes,")"]},s.id))]}),e.jsxs("button",{className:"save-btn",onClick:P,disabled:!v,children:[e.jsx(S,{size:16})," Add"]})]}),n.length===0?e.jsx("p",{style:{opacity:.6,textAlign:"center",padding:24},children:"No members yet. Add any API key above."}):e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse"},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid rgba(255,255,255,0.08)"},children:[e.jsx("th",{style:r,children:"Name"}),e.jsx("th",{style:r,children:"Role"}),e.jsx("th",{style:r,children:"Scopes"}),e.jsx("th",{style:{...r,textAlign:"right"},children:"Actions"})]})}),e.jsx("tbody",{children:n.map(s=>e.jsxs("tr",{style:{borderBottom:"1px solid rgba(255,255,255,0.05)"},children:[e.jsx("td",{style:d,children:s.name}),e.jsx("td",{style:d,children:s.role}),e.jsx("td",{style:d,children:e.jsx("code",{style:{fontSize:"0.85em",opacity:.8},children:s.scopes})}),e.jsx("td",{style:{...d,textAlign:"right"},children:e.jsxs("button",{className:"reset-btn",onClick:()=>E(s.id),style:{color:"var(--accent-red)"},children:[e.jsx(B,{size:14})," Remove"]})})]},s.id))})]})]})]}),e.jsxs("section",{children:[e.jsxs("h3",{style:{marginBottom:12},children:["Devices (",g.length,")"]}),e.jsxs("div",{className:"setting-card",style:{padding:12},children:[e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12},children:[e.jsxs("select",{value:N,onChange:s=>D(s.target.value),style:{...A,flex:1},children:[e.jsx("option",{value:"",children:"Select a shared-pool device to assign…"}),b.map(s=>e.jsxs("option",{value:s.udid,children:[s.name," (",s.platform,") — ",s.udid.slice(0,12),"…"]},s.udid))]}),e.jsxs("button",{className:"save-btn",onClick:R,disabled:!N,children:[e.jsx(S,{size:16})," Assign"]})]}),g.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("table",{style:{width:"100%",borderCollapse:"collapse"},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid rgba(255,255,255,0.08)"},children:[e.jsx("th",{style:r,children:"Name"}),e.jsx("th",{style:r,children:"Platform"}),e.jsx("th",{style:r,children:"UDID"}),e.jsx("th",{style:{...r,textAlign:"right"},children:"Actions"})]})}),e.jsx("tbody",{children:g.map(s=>e.jsxs("tr",{style:{borderBottom:"1px solid rgba(255,255,255,0.05)"},children:[e.jsxs("td",{style:d,children:[e.jsx(O,{size:14,style:{marginRight:6,verticalAlign:"middle"}}),s.name]}),e.jsx("td",{style:d,children:s.platform}),e.jsx("td",{style:d,children:e.jsxs("code",{style:{fontSize:"0.85em"},children:[s.udid.slice(0,14),"…"]})}),e.jsx("td",{style:{...d,textAlign:"right"},children:e.jsx("button",{className:"reset-btn",onClick:()=>$(s.udid),children:"Return to shared pool"})})]},s.udid))})]})]})]})]})]})},G=({onClose:i,title:o,children:t})=>e.jsx("div",{onClick:i,style:{position:"fixed",inset:0,background:"rgba(0,0,0,0.6)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:2e3},children:e.jsxs("div",{onClick:n=>n.stopPropagation(),style:{background:"var(--bg-main, #0d0d12)",border:"1px solid rgba(255,255,255,0.08)",borderRadius:10,padding:24,width:"min(480px, 90vw)"},children:[e.jsxs("div",{style:{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16},children:[e.jsx("h3",{style:{margin:0},children:o}),e.jsx("button",{onClick:i,style:{background:"none",border:"none",cursor:"pointer",color:"inherit"},"aria-label":"Close",children:e.jsx(K,{size:20})})]}),t]})}),r={textAlign:"left",padding:"12px 16px",fontSize:"0.85em",textTransform:"uppercase",letterSpacing:"0.05em",opacity:.7,fontWeight:600},d={padding:"12px 16px",verticalAlign:"top"},H={fontSize:"0.85em",textTransform:"uppercase",letterSpacing:"0.05em",opacity:.7,marginBottom:6,fontWeight:600},A={width:"100%",padding:"8px 12px",background:"rgba(0,0,0,0.3)",border:"1px solid rgba(255,255,255,0.1)",borderRadius:6,color:"inherit",fontSize:"0.95em",boxSizing:"border-box"};export{se as Teams};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as i,j as e,k,i as A,C as _}from"./index-
|
|
1
|
+
import{r as i,j as e,k,i as A,C as _}from"./index-Do3u3FMx.js";import{X as n}from"./index-C1DBaoSh.js";import{T as g}from"./trash-2-DEafUZlN.js";import{Z as W}from"./zap-DCsIYdwr.js";import{P as O}from"./plus-DecbehZh.js";const v=[{id:"device_offline",label:"Device Offline",icon:e.jsx(_,{size:14,className:"text-red-400"})},{id:"device_new",label:"New Device",icon:e.jsx(O,{size:14,className:"text-green-400"})},{id:"session_failed",label:"Session Failed",icon:e.jsx(g,{size:14,className:"text-orange-400"})}],J=()=>{const[x,w]=i.useState([]),[t,p]=i.useState(""),[r,j]=i.useState(["device_offline","session_failed"]),[N,u]=i.useState(!1),[d,o]=i.useState("idle"),[c,b]=i.useState(!1),[f,h]=i.useState("");i.useEffect(()=>{m()},[]);const m=async()=>{try{const s=await n.getWebhookConfigs();w(s||[])}catch(s){console.error("Failed to load webhook configs",s)}},y=async()=>{if(t){u(!0);try{await n.addWebhookConfig(t,r,"slack",f||void 0),p(""),h(""),b(!1),j(["device_offline","session_failed"]),await m()}catch(s){console.error("Failed to add webhook",s)}finally{u(!1)}}},S=async s=>{try{await n.deleteWebhookConfig(s),await m()}catch(a){console.error("Failed to delete webhook",a)}},C=async()=>{if(t){o("idle");try{await n.testWebhook(t,"slack"),o("success"),setTimeout(()=>o("idle"),3e3)}catch{o("error"),setTimeout(()=>o("idle"),3e3)}}},T=s=>{j(a=>a.includes(s)?a.filter(l=>l!==s):[...a,s])},E=s=>{h(a=>a+`{{${s}}} `)};return e.jsxs("div",{className:"webhook-settings-container",children:[e.jsx("div",{className:"scanline",style:{position:"absolute",inset:0,pointerEvents:"none",opacity:.05,zIndex:1001}}),e.jsxs("div",{className:"webhook-header",children:[e.jsxs("div",{className:"webhook-title",children:[e.jsx(k,{className:"webhook-icon",size:20}),e.jsx("h2",{children:"Notification Webhooks"})]}),e.jsx("p",{className:"webhook-subtitle",children:"Configure Slack or generic webhooks to receive alerts for critical infrastructure events."})]}),e.jsxs("div",{className:"webhook-list",children:[x.map(s=>e.jsxs("div",{className:"webhook-card",children:[e.jsxs("div",{className:"webhook-card-header",children:[e.jsxs("div",{className:"webhook-url-display",children:[e.jsx("span",{className:"platform-tag",children:s.payloadTemplate?"CUSTOM":"SLACK"}),e.jsx("span",{className:"url-text",children:s.url})]}),e.jsx("button",{className:"delete-btn",onClick:()=>S(s.id),children:e.jsx(g,{size:16})})]}),e.jsx("div",{className:"webhook-events-list",children:JSON.parse(s.events).map(a=>{const l=v.find(z=>z.id===a);return e.jsxs("span",{className:"event-pill",children:[l==null?void 0:l.icon,(l==null?void 0:l.label)||a]},a)})})]},s.id)),x.length===0&&e.jsxs("div",{className:"empty-webhook-state",children:[e.jsx(k,{size:48,className:"empty-icon"}),e.jsx("p",{children:"No webhooks configured yet."})]})]}),e.jsxs("div",{className:"add-webhook-form",children:[e.jsxs("div",{className:"form-scrollable-content",children:[e.jsx("h3",{children:"Add New Webhook"}),e.jsx("div",{className:"form-group",children:e.jsx("input",{type:"text",className:"webhook-input",placeholder:"https://hooks.slack.com/services/...",value:t,onChange:s=>p(s.target.value)})}),e.jsxs("div",{className:"events-selection",children:[e.jsx("label",{children:"Trigger Events:"}),e.jsx("div",{className:"events-grid",children:v.map(s=>e.jsxs("div",{className:`event-checkbox ${r.includes(s.id)?"selected":""}`,onClick:()=>T(s.id),children:[s.icon,e.jsx("span",{children:s.label}),r.includes(s.id)&&e.jsx(A,{size:14,className:"check-icon"})]},s.id))})]}),e.jsxs("div",{className:"template-section",children:[e.jsxs("div",{className:"template-header",onClick:()=>b(!c),children:[e.jsxs("div",{className:"template-toggle",children:[e.jsx(W,{size:16,className:c?"text-yellow-400":"text-gray-400"}),e.jsx("span",{children:"Use Custom Payload (Optional)"})]}),e.jsx("span",{className:"toggle-indicator",children:c?"−":"+"})]}),c&&e.jsxs("div",{className:"template-editor",children:[e.jsxs("p",{className:"template-hint",children:["Define a JSON or text template. Use variables like ",e.jsx("code",{children:"{{udid}}"})," to insert dynamic data."]}),e.jsx("div",{className:"variable-chips",children:["udid","host","name","sessionId","failureReason","eventType","platform"].map(s=>e.jsx("span",{className:"variable-chip",onClick:()=>E(s),children:s},s))}),e.jsx("textarea",{className:"template-textarea",placeholder:'Example JSON: { "text": "Alert: Device {{udid}} is offline!" }',value:f,onChange:s=>h(s.target.value),rows:3})]})]})]}),e.jsxs("div",{className:"form-actions",children:[e.jsx("button",{className:`test-btn ${d}`,onClick:C,disabled:!t,children:d==="success"?"Sent!":d==="error"?"Failed":"Test Payload"}),e.jsx("button",{className:"add-btn",onClick:y,disabled:!t||N,children:N?"Saving...":"Save Configuration"})]})]})]})};export{J as WebhookSettings};
|
package/lib/public/index.html
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
Learn how to configure a non-root public URL by running `npm run build`.
|
|
25
25
|
-->
|
|
26
26
|
<title>Xenon</title>
|
|
27
|
-
<script type="module" crossorigin src="/xenon/assets/index-
|
|
27
|
+
<script type="module" crossorigin src="/xenon/assets/index-Do3u3FMx.js"></script>
|
|
28
28
|
<link rel="stylesheet" crossorigin href="/xenon/assets/index-qzCez_kk.css">
|
|
29
29
|
</head>
|
|
30
30
|
|
|
@@ -48,6 +48,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
48
48
|
exports.XENON_CAPABILITIES = void 0;
|
|
49
49
|
exports.androidCapabilities = androidCapabilities;
|
|
50
50
|
exports.iOSCapabilities = iOSCapabilities;
|
|
51
|
+
exports.extractTeamCap = extractTeamCap;
|
|
52
|
+
exports.extractAccessKeyCap = extractAccessKeyCap;
|
|
51
53
|
exports.getXenonCapabilities = getXenonCapabilities;
|
|
52
54
|
const get_port_1 = __importDefault(require("get-port"));
|
|
53
55
|
const lodash_1 = __importDefault(require("lodash"));
|
|
@@ -151,6 +153,34 @@ function iOSCapabilities(caps, freeDevice) {
|
|
|
151
153
|
deleteMatch.forEach((value) => deleteAlwaysMatch(caps, value));
|
|
152
154
|
});
|
|
153
155
|
}
|
|
156
|
+
function extractTeamCap(caps) {
|
|
157
|
+
var _a;
|
|
158
|
+
const merged = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
|
|
159
|
+
const prefixes = ['xenon:', 'xe:', 'appium:', ''];
|
|
160
|
+
const names = ['teamId', 'team_id', 'team'];
|
|
161
|
+
for (const prefix of prefixes) {
|
|
162
|
+
for (const name of names) {
|
|
163
|
+
const v = merged[prefix ? `${prefix}${name}` : name];
|
|
164
|
+
if (typeof v === 'string' && v.length > 0)
|
|
165
|
+
return v;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
function extractAccessKeyCap(caps) {
|
|
171
|
+
var _a;
|
|
172
|
+
const merged = Object.assign({}, ((_a = caps.firstMatch) === null || _a === void 0 ? void 0 : _a[0]) || {}, caps.alwaysMatch || {});
|
|
173
|
+
const prefixes = ['xenon:', 'xe:', 'appium:', ''];
|
|
174
|
+
const names = ['accessKey', 'access_key'];
|
|
175
|
+
for (const prefix of prefixes) {
|
|
176
|
+
for (const name of names) {
|
|
177
|
+
const v = merged[prefix ? `${prefix}${name}` : name];
|
|
178
|
+
if (typeof v === 'string' && v.length > 0)
|
|
179
|
+
return v;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
154
184
|
function getXenonCapabilities(caps) {
|
|
155
185
|
const mergedCapabilites = Object.assign({}, caps.firstMatch[0], caps.alwaysMatch);
|
|
156
186
|
const getAnyCap = (snake, camel) => {
|
package/lib/src/app/index.js
CHANGED
|
@@ -66,6 +66,7 @@ const webhook_1 = __importDefault(require("./routers/webhook"));
|
|
|
66
66
|
const reservation_1 = __importDefault(require("./routers/reservation"));
|
|
67
67
|
const config_2 = __importDefault(require("./routers/config"));
|
|
68
68
|
const apikeys_1 = require("./routers/apikeys");
|
|
69
|
+
const teams_1 = require("./routers/teams");
|
|
69
70
|
const auth_1 = require("./routers/auth");
|
|
70
71
|
const processes_1 = require("./routers/processes");
|
|
71
72
|
const apiKeyMiddleware_1 = require("../middleware/apiKeyMiddleware");
|
|
@@ -223,6 +224,8 @@ function createRouter(pluginArgs) {
|
|
|
223
224
|
apiRouter.use((0, rateLimitMiddleware_1.rateLimitMiddleware)());
|
|
224
225
|
// Admin: API key management
|
|
225
226
|
apiRouter.use('/apikeys', (0, apikeys_1.apiKeysRouter)());
|
|
227
|
+
// Admin: team membership and device→team ownership
|
|
228
|
+
apiRouter.use('/teams', (0, teams_1.teamsRouter)());
|
|
226
229
|
// Admin: running process snapshot (ops debugging)
|
|
227
230
|
apiRouter.use('/processes', (0, processes_1.processesRouter)());
|
|
228
231
|
// Exposes plugin CLI args (may include host, hub URL, etc.) — auth-gated.
|
|
@@ -17,12 +17,15 @@ const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
|
17
17
|
function apiKeysRouter() {
|
|
18
18
|
const r = (0, express_1.Router)();
|
|
19
19
|
const svc = typedi_1.Container.get(ApiKeyService_1.ApiKeyService);
|
|
20
|
+
r.get('/', (0, scopeGuard_1.scopeGuard)(['admin']), (_req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
res.json(yield svc.list());
|
|
22
|
+
}));
|
|
20
23
|
r.post('/', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
21
|
-
const { name, scopes, rateLimit } = req.body;
|
|
24
|
+
const { name, scopes, rateLimit, teamId } = req.body;
|
|
22
25
|
if (!name || !Array.isArray(scopes) || scopes.length === 0) {
|
|
23
26
|
return res.status(400).json({ error: 'name and scopes required' });
|
|
24
27
|
}
|
|
25
|
-
const { id, raw } = yield svc.create({ name, scopes, rateLimit });
|
|
28
|
+
const { id, raw } = yield svc.create({ name, scopes, rateLimit, teamId: teamId !== null && teamId !== void 0 ? teamId : null });
|
|
26
29
|
res.json({ id, key: raw });
|
|
27
30
|
}));
|
|
28
31
|
r.delete('/:id', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -54,13 +54,24 @@ const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
|
54
54
|
const logger_1 = __importDefault(require("../../logger"));
|
|
55
55
|
const device_managers_1 = require("../../device-managers");
|
|
56
56
|
const typedi_1 = require("typedi");
|
|
57
|
+
const prisma_1 = require("../../prisma");
|
|
57
58
|
const store = device_store_1.DeviceStoreFactory.getStore();
|
|
58
59
|
const pendingStore = device_store_1.DeviceStoreFactory.getPendingSessionStore();
|
|
59
60
|
const SERVER_UP_TIME = new Date().toISOString();
|
|
60
61
|
function getDevices(request, response) {
|
|
61
62
|
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
-
var _a;
|
|
63
|
+
var _a, _b;
|
|
63
64
|
let devices = yield store.getAllDevices();
|
|
65
|
+
// Non-admin callers only see devices in their team + shared (null) pool.
|
|
66
|
+
// Admin keys (including the bootstrap key) see everything.
|
|
67
|
+
const caller = request.apiKey;
|
|
68
|
+
if (caller) {
|
|
69
|
+
const scopes = new Set(caller.scopes.split(',').map((s) => s.trim()));
|
|
70
|
+
if (!scopes.has('admin')) {
|
|
71
|
+
const myTeam = (_a = caller.teamId) !== null && _a !== void 0 ? _a : null;
|
|
72
|
+
devices = devices.filter((d) => !d.teamId || d.teamId === myTeam);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
64
75
|
const { sessionId } = request.query;
|
|
65
76
|
if (sessionId) {
|
|
66
77
|
return response.json(devices.find((value) => value.session_id === sessionId));
|
|
@@ -71,7 +82,7 @@ function getDevices(request, response) {
|
|
|
71
82
|
const dashboardPluginUrl = request['dashboard-plugin-url'];
|
|
72
83
|
if (dashboardPluginUrl) {
|
|
73
84
|
const response = yield InternalHttpClient_1.InternalHttpClient.get(`${dashboardPluginUrl}/api/sessions?start_time=${SERVER_UP_TIME}`);
|
|
74
|
-
const sessions = ((
|
|
85
|
+
const sessions = ((_b = response === null || response === void 0 ? void 0 : response.result) === null || _b === void 0 ? void 0 : _b.rows) || [];
|
|
75
86
|
const deviceSessionMap = {};
|
|
76
87
|
sessions.forEach((session) => {
|
|
77
88
|
if (!deviceSessionMap[session.udid]) {
|
|
@@ -91,6 +102,7 @@ function getDevices(request, response) {
|
|
|
91
102
|
}
|
|
92
103
|
function getDeviceByPlatform(request, response) {
|
|
93
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
105
|
+
var _a;
|
|
94
106
|
const { platform } = request.params;
|
|
95
107
|
const { deviceType, booted } = request.query;
|
|
96
108
|
if (!platform || ['ios', 'android'].indexOf(platform.toLowerCase()) < 0) {
|
|
@@ -105,6 +117,14 @@ function getDeviceByPlatform(request, response) {
|
|
|
105
117
|
if (!lodash_1.default.isNil(booted)) {
|
|
106
118
|
devices = devices.filter((d) => d.state === 'Booted');
|
|
107
119
|
}
|
|
120
|
+
const caller = request.apiKey;
|
|
121
|
+
if (caller) {
|
|
122
|
+
const scopes = new Set(caller.scopes.split(',').map((s) => s.trim()));
|
|
123
|
+
if (!scopes.has('admin')) {
|
|
124
|
+
const myTeam = (_a = caller.teamId) !== null && _a !== void 0 ? _a : null;
|
|
125
|
+
devices = devices.filter((d) => !d.teamId || d.teamId === myTeam);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
108
128
|
return response.status(200).send(devices);
|
|
109
129
|
});
|
|
110
130
|
}
|
|
@@ -283,6 +303,29 @@ function updateTags(request, response) {
|
|
|
283
303
|
response.status(200).json({ success: true });
|
|
284
304
|
});
|
|
285
305
|
}
|
|
306
|
+
function assignDeviceToTeam(request, response) {
|
|
307
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
308
|
+
const { udid } = request.params;
|
|
309
|
+
const { teamId } = request.body;
|
|
310
|
+
if (teamId) {
|
|
311
|
+
const team = yield prisma_1.prisma.team.findUnique({ where: { id: teamId }, select: { id: true } });
|
|
312
|
+
if (!team)
|
|
313
|
+
return response.status(404).json({ error: 'team not found' });
|
|
314
|
+
}
|
|
315
|
+
const result = yield prisma_1.prisma.device.updateMany({
|
|
316
|
+
where: { udid },
|
|
317
|
+
data: { teamId: teamId || null },
|
|
318
|
+
});
|
|
319
|
+
if (result.count === 0)
|
|
320
|
+
return response.status(404).json({ error: 'device not found' });
|
|
321
|
+
// Refresh in-memory store so subsequent allocation sees the change immediately.
|
|
322
|
+
const rows = yield prisma_1.prisma.device.findMany({ where: { udid } });
|
|
323
|
+
for (const row of rows) {
|
|
324
|
+
yield store.updateDevice(row.udid, row.host, { teamId: row.teamId });
|
|
325
|
+
}
|
|
326
|
+
response.json({ ok: true, updated: result.count });
|
|
327
|
+
});
|
|
328
|
+
}
|
|
286
329
|
/**
|
|
287
330
|
* Returns active session statistics
|
|
288
331
|
*/
|
|
@@ -345,6 +388,7 @@ function register(router, pluginArgs) {
|
|
|
345
388
|
router.post('/block', (0, scopeGuard_1.scopeGuard)(['devices']), blockDevice);
|
|
346
389
|
router.post('/unblock', (0, scopeGuard_1.scopeGuard)(['devices']), unBlockDevice);
|
|
347
390
|
router.post('/device/tags', (0, scopeGuard_1.scopeGuard)(['devices']), updateTags);
|
|
391
|
+
router.put('/device/:udid/team', (0, scopeGuard_1.scopeGuard)(['admin']), assignDeviceToTeam);
|
|
348
392
|
// session related
|
|
349
393
|
router.get('/queue/length', getQueuedSessionLength);
|
|
350
394
|
router.get('/queue', getQueuedSessionRequests);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.teamsRouter = teamsRouter;
|
|
13
|
+
const express_1 = require("express");
|
|
14
|
+
const typedi_1 = require("typedi");
|
|
15
|
+
const TeamService_1 = require("../../services/TeamService");
|
|
16
|
+
const scopeGuard_1 = require("../../middleware/scopeGuard");
|
|
17
|
+
function teamsRouter() {
|
|
18
|
+
const r = (0, express_1.Router)();
|
|
19
|
+
const svc = typedi_1.Container.get(TeamService_1.TeamService);
|
|
20
|
+
r.get('/', (0, scopeGuard_1.scopeGuard)(['admin']), (_req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
21
|
+
res.json(yield svc.list());
|
|
22
|
+
}));
|
|
23
|
+
r.post('/', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
const { name } = req.body;
|
|
25
|
+
if (!name)
|
|
26
|
+
return res.status(400).json({ error: 'name required' });
|
|
27
|
+
try {
|
|
28
|
+
const team = yield svc.create(name);
|
|
29
|
+
res.json(team);
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
if (e.code === 'P2002')
|
|
33
|
+
return res.status(409).json({ error: 'team name already exists' });
|
|
34
|
+
res.status(400).json({ error: e.message });
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
r.delete('/:id', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
try {
|
|
39
|
+
yield svc.delete(req.params.id);
|
|
40
|
+
res.json({ ok: true });
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
res.status(400).json({ error: e.message });
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
r.get('/:id/members', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
47
|
+
res.json(yield svc.listMembers(req.params.id));
|
|
48
|
+
}));
|
|
49
|
+
r.post('/:id/members', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
50
|
+
const { apiKeyId, role } = req.body;
|
|
51
|
+
if (!apiKeyId)
|
|
52
|
+
return res.status(400).json({ error: 'apiKeyId required' });
|
|
53
|
+
try {
|
|
54
|
+
yield svc.addMember(req.params.id, apiKeyId, role || 'member');
|
|
55
|
+
res.json({ ok: true });
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
res.status(400).json({ error: e.message });
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
r.delete('/:id/members/:apiKeyId', (0, scopeGuard_1.scopeGuard)(['admin']), (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
yield svc.removeMember(req.params.id, req.params.apiKeyId);
|
|
63
|
+
res.json({ ok: true });
|
|
64
|
+
}));
|
|
65
|
+
return r;
|
|
66
|
+
}
|
|
@@ -95,6 +95,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
95
95
|
}
|
|
96
96
|
onSessionStarted(capabilities, session, device) {
|
|
97
97
|
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
var _a;
|
|
98
99
|
// Store device info for this session
|
|
99
100
|
this.sessionToDevice.set(session.getId(), device);
|
|
100
101
|
const createOptions = {
|
|
@@ -144,6 +145,7 @@ let DashboardEventManager = class DashboardEventManager {
|
|
|
144
145
|
device_name: device.name,
|
|
145
146
|
trace_id: traceId,
|
|
146
147
|
status: 'running', // Principal Polish: Set status explicitly
|
|
148
|
+
api_key_id: (_a = session.apiKeyId) !== null && _a !== void 0 ? _a : null,
|
|
147
149
|
};
|
|
148
150
|
yield prisma_1.prisma.session.create({
|
|
149
151
|
data: createData,
|
|
@@ -81,6 +81,18 @@ class LokiDeviceStore {
|
|
|
81
81
|
if (!(device.host || '').toLowerCase().includes(hostFilter))
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
|
+
// Team scoping (see prisma-store.ts for the DB equivalent).
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(filterOptions, 'callerTeamId')) {
|
|
86
|
+
const caller = filterOptions.callerTeamId;
|
|
87
|
+
if (caller) {
|
|
88
|
+
if (device.teamId && device.teamId !== caller)
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
if (device.teamId)
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
84
96
|
// Session ID Filter
|
|
85
97
|
if (filterOptions.session_id) {
|
|
86
98
|
if (device.session_id !== filterOptions.session_id)
|
|
@@ -126,6 +126,13 @@ class PrismaDeviceStore {
|
|
|
126
126
|
if (filterOptions.filterByHost) {
|
|
127
127
|
where.host = { contains: filterOptions.filterByHost };
|
|
128
128
|
}
|
|
129
|
+
// Team scoping: caller with a team sees devices in its team + shared pool;
|
|
130
|
+
// caller without a team sees shared pool only. Admin callers pass undefined.
|
|
131
|
+
if (Object.prototype.hasOwnProperty.call(filterOptions, 'callerTeamId')) {
|
|
132
|
+
where.teamId = filterOptions.callerTeamId
|
|
133
|
+
? { in: [null, filterOptions.callerTeamId] }
|
|
134
|
+
: null;
|
|
135
|
+
}
|
|
129
136
|
// 1. Fetch narrowed result set from Database
|
|
130
137
|
const devices = yield this.prisma.device.findMany({ where });
|
|
131
138
|
let results = devices.map((d) => this.toIDevice(d));
|
package/lib/src/device-utils.js
CHANGED
|
@@ -149,10 +149,16 @@ function isDeviceConfigPathAbsolute(path) {
|
|
|
149
149
|
* @param capability
|
|
150
150
|
* @returns
|
|
151
151
|
*/
|
|
152
|
-
function allocateDeviceForSession(capability, deviceTimeOutMs, deviceQueryIntervalMs, pluginArgs) {
|
|
152
|
+
function allocateDeviceForSession(capability, deviceTimeOutMs, deviceQueryIntervalMs, pluginArgs, callerTeamId) {
|
|
153
153
|
return __awaiter(this, void 0, void 0, function* () {
|
|
154
154
|
const firstMatch = Object.assign({}, capability.firstMatch[0], capability.alwaysMatch);
|
|
155
155
|
const filters = getDeviceFiltersFromCapability(firstMatch, pluginArgs);
|
|
156
|
+
// callerTeamId === undefined → unscoped (admin / auth-disabled / back-compat).
|
|
157
|
+
// callerTeamId === null → caller has no team, only shared pool visible.
|
|
158
|
+
// callerTeamId === '<uuid>' → team + shared pool visible.
|
|
159
|
+
if (callerTeamId !== undefined) {
|
|
160
|
+
filters.callerTeamId = callerTeamId;
|
|
161
|
+
}
|
|
156
162
|
const timeout = firstMatch[customCapability.deviceTimeOut] || deviceTimeOutMs;
|
|
157
163
|
const intervalBetweenAttempts = firstMatch[customCapability.deviceQueryInterval] || deviceQueryIntervalMs;
|
|
158
164
|
let device = null;
|