@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.
Files changed (58) hide show
  1. package/README.md +17 -0
  2. package/lib/package.json +1 -1
  3. package/lib/public/assets/{Layouts-D6IPfwoe.js → Layouts-BV_fQEKa.js} +1 -1
  4. package/lib/public/assets/{ai-settings-CflyFKan.js → ai-settings-BnoeJplt.js} +2 -2
  5. package/lib/public/assets/api-keys-TLWTJYrE.js +1 -0
  6. package/lib/public/assets/{apps-Da4dvQ1J.js → apps-BGCcB8_p.js} +2 -2
  7. package/lib/public/assets/arrow-left-B6-SH02F.js +6 -0
  8. package/lib/public/assets/{badge-BNR9umdu.js → badge-BtQKmQsj.js} +1 -1
  9. package/lib/public/assets/button-CNLKazSa.js +16 -0
  10. package/lib/public/assets/{calendar-fehdBtun.js → calendar-CPNkf0zl.js} +1 -1
  11. package/lib/public/assets/{clock-DrpxSvCL.js → clock-C8ZaLIQA.js} +1 -1
  12. package/lib/public/assets/copy-DCAmOyY9.js +11 -0
  13. package/lib/public/assets/{cpu-tuyMVZ4I.js → cpu-D3PzrmhN.js} +1 -1
  14. package/lib/public/assets/{device-explorer-DOfRH3zm.js → device-explorer-BfFe3bRe.js} +36 -41
  15. package/lib/public/assets/{index-BaTiUCeH.js → index-Do3u3FMx.js} +38 -28
  16. package/lib/public/assets/{lock-C6CoqSr2.js → lock-DNzAbx6Q.js} +1 -1
  17. package/lib/public/assets/{maintenance-settings-CM2oC7-i.js → maintenance-settings-yHAKnfz_.js} +2 -2
  18. package/lib/public/assets/{mouse-pointer-2-CXdnjXIg.js → mouse-pointer-2-B0KBsfLG.js} +1 -1
  19. package/lib/public/assets/{plus-B4B1Hukt.js → plus-DecbehZh.js} +1 -1
  20. package/lib/public/assets/{session-dashboard-B5OPMTz5.js → session-dashboard-CJvDLc6D.js} +17 -22
  21. package/lib/public/assets/settings-DjTQTjSq.js +1 -0
  22. package/lib/public/assets/shield-alert-Br1Ebl1G.js +6 -0
  23. package/lib/public/assets/teams-LILlQka3.js +1 -0
  24. package/lib/public/assets/{trash-2-NJMZJ2Ol.js → trash-2-DEafUZlN.js} +1 -1
  25. package/lib/public/assets/{useSocket-Ct2wo7P2.js → useSocket-Dzv_TTsO.js} +1 -1
  26. package/lib/public/assets/{webhook-settings-Cz35-QJ7.js → webhook-settings-CVwvtwI7.js} +1 -1
  27. package/lib/public/assets/{zap-CssSMAN5.js → zap-DCsIYdwr.js} +1 -1
  28. package/lib/public/index.html +1 -1
  29. package/lib/src/XenonCapabilityManager.js +30 -0
  30. package/lib/src/app/index.js +3 -0
  31. package/lib/src/app/routers/apikeys.js +5 -2
  32. package/lib/src/app/routers/grid.js +46 -2
  33. package/lib/src/app/routers/teams.js +66 -0
  34. package/lib/src/dashboard/event-manager.js +2 -0
  35. package/lib/src/data-service/device-store.js +12 -0
  36. package/lib/src/data-service/prisma-store.js +7 -0
  37. package/lib/src/device-utils.js +7 -1
  38. package/lib/src/generated/client/edge.js +18 -7
  39. package/lib/src/generated/client/index-browser.js +15 -4
  40. package/lib/src/generated/client/index.d.ts +2613 -391
  41. package/lib/src/generated/client/index.js +18 -7
  42. package/lib/src/generated/client/package.json +1 -1
  43. package/lib/src/generated/client/schema.prisma +18 -0
  44. package/lib/src/generated/client/wasm.js +15 -4
  45. package/lib/src/logger.js +17 -0
  46. package/lib/src/middleware/apiKeyMiddleware.js +22 -2
  47. package/lib/src/services/ApiKeyService.js +20 -1
  48. package/lib/src/services/ServerManager.js +10 -3
  49. package/lib/src/services/SessionLifecycleService.js +53 -4
  50. package/lib/src/services/TeamService.js +100 -0
  51. package/lib/src/sessions/XenonSession.js +3 -0
  52. package/lib/tsconfig.tsbuildinfo +1 -1
  53. package/package.json +1 -1
  54. package/prisma/migrations/20260424033547_add_teams/migration.sql +101 -0
  55. package/prisma/schema.prisma +18 -0
  56. package/lib/public/assets/button-hZFV1ypT.js +0 -26
  57. package/lib/public/assets/settings-BTHP7fj3.js +0 -1
  58. /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,4 +1,4 @@
1
- import{c as a}from"./index-BaTiUCeH.js";/**
1
+ import{c as a}from"./index-Do3u3FMx.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1,4 +1,4 @@
1
- import{c as V,r as E}from"./index-BaTiUCeH.js";/**
1
+ import{c as V,r as E}from"./index-Do3u3FMx.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -1 +1 @@
1
- import{r as i,j as e,k,i as A,C as _}from"./index-BaTiUCeH.js";import{X as n}from"./index-C1DBaoSh.js";import{T as g}from"./trash-2-NJMZJ2Ol.js";import{Z as W}from"./zap-CssSMAN5.js";import{P as O}from"./plus-B4B1Hukt.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};
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};
@@ -1,4 +1,4 @@
1
- import{c as a}from"./index-BaTiUCeH.js";/**
1
+ import{c as a}from"./index-Do3u3FMx.js";/**
2
2
  * @license lucide-react v0.555.0 - ISC
3
3
  *
4
4
  * This source code is licensed under the ISC license.
@@ -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-BaTiUCeH.js"></script>
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) => {
@@ -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 = ((_a = response === null || response === void 0 ? void 0 : response.result) === null || _a === void 0 ? void 0 : _a.rows) || [];
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));
@@ -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;