1import{ag as _,u as L,r as t,j as e,U as A,C as v,i as g,k as j,m as f,B as d,af as y,ad as b,I as G,s as K,G as o}from"./index-DhwrhnZy.js";import{Q}from"./qr-code-BELcD2KC.js";import{S as M}from"./smartphone-D9rmBN9Y.js";function z(){const m=_(),{data:a,isLoading:q}=L({queryKey:["/api/gary/devices"],queryFn:async()=>(await o("GET","/api/gary/devices")).json()}),[i,x]=t.useState(null),[N,w]=t.useState(!1),[D,I]=t.useState(0),r=t.useRef(null);t.useEffect(()=>{if(i)return r.current&&clearInterval(r.current),r.current=setInterval(()=>{const s=Math.max(0,Math.round((i.expiresAt-Date.now())/1e3));I(s),s===0&&(x(null),m.invalidateQueries({queryKey:["/api/gary/devices"]}))},500),()=>{r.current&&clearInterval(r.current)}},[i,m]);async function P(){w(!0);try{const n=await(await o("POST","/api/gary/devices/pairing-codes",{})).json();x({png:n.qr_png,expiresAt:new Date(n.expires_at).getTime()})}finally{w(!1)}}const[u,S]=t.useState(""),[l,c]=t.useState("idle"),[k,C]=t.useState(null);async function T(){const s=u.trim();if(s){c("sending"),C(null);try{await o("POST","/api/gary/app-invite",{email:s}),c("sent"),S(""),setTimeout(()=>c("idle"),4e3)}catch(n){const p=String((n==null?void 0:n.message)||"");C(p.includes("invite_rate_limited")?"Too many invites this hour â try again later.":p.includes("valid_email_required")?"That doesn't look like an email address.":p.includes("email_not_configured")?"Email sending isn't configured on this server.":"Couldn't send the invite. Try again in a moment."),c("error")}}}async function R(s){confirm("Revoke this device? The Gary app on it stops working immediately.")&&(await o("POST",`/api/gary/devices/${s}/revoke`),m.invalidateQueries({queryKey:["/api/gary/devices"]}))}const E=((a==null?void 0:a.devices)??[]).filter(s=>s.status==="active"),h=((a==null?void 0:a.devices)??[]).filter(s=>s.status==="revoked");return e.jsx(A,{children:e.jsxs("div",{className:"max-w-3xl mx-auto p-6 space-y-6",children:[e.jsxs("div",{children:[e.jsx("h1",{className:"text-2xl font-semibold",children:"Paired devices"}),e.jsx("p",{className:"text-muted-foreground mt-1",children:"Pair a phone by scanning a one-time QR from the Gary app. Each code works once and expires in 60 seconds."})]}),e.jsxs(v,{children:[e.jsx(g,{children:e.jsxs(j,{className:"text-lg flex items-center gap-2",children:[e.jsx(Q,{className:"h-5 w-5"})," Pair a new phone"]})}),e.jsx(f,{children:i?e.jsxs("div",{className:"flex flex-col items-center gap-3 py-2",children:[i.png?e.jsx("img",{src:i.png,alt:"Pairing QR code",className:"w-64 h-64 rounded-md border"}):e.jsx("div",{className:"text-sm text-red-500",children:"QR image unavailable â mint again."}),e.jsxs("div",{className:"text-sm text-muted-foreground",children:["Scan with the Gary app · expires in ",e.jsxs("span",{className:"font-mono",children:[D,"s"]})]}),e.jsx(d,{variant:"ghost",size:"sm",onClick:()=>x(null),children:"Cancel"})]}):e.jsxs(d,{onClick:P,disabled:N,children:[N?e.jsx(y,{className:"h-4 w-4 animate-spin mr-2"}):e.jsx(Q,{className:"h-4 w-4 mr-2"}),"Generate pairing code"]})})]}),e.jsxs(v,{children:[e.jsx(g,{children:e.jsxs(j,{className:"text-lg flex items-center gap-2",children:[e.jsx(b,{className:"h-5 w-5"})," Email someone a setup link"]})}),e.jsxs(f,{children:[e.jsx("p",{className:"text-sm text-muted-foreground mb-3",children:"They get the App Store link and a setup page that walks them through creating an account, connecting email and calendar, and scanning a pairing code. The code is minted when they arrive â nothing in the email can expire."}),e.jsxs("div",{className:"flex gap-2",children:[e.jsx(G,{type:"email",placeholder:"[email protected]",value:u,onChange:s=>{S(s.target.value),l==="error"&&c("idle")},onKeyDown:s=>{s.key==="Enter"&&T()},className:"max-w-xs","data-testid":"input-invite-email"}),e.jsxs(d,{onClick:T,disabled:l==="sending"||!u.trim(),"data-testid":"button-send-invite",children:[l==="sending"?e.jsx(y,{className:"h-4 w-4 animate-spin mr-2"}):e.jsx(b,{className:"h-4 w-4 mr-2"}),l==="sent"?"Sent!":"Send invite"]})]}),l==="error"&&k&&e.jsx("p",{className:"text-sm text-red-500 mt-2","data-testid":"text-invite-error",children:k})]})]}),e.jsxs(v,{children:[e.jsx(g,{children:e.jsxs(j,{className:"text-lg flex items-center gap-2",children:[e.jsx(M,{className:"h-5 w-5"})," Devices"]})}),e.jsxs(f,{children:[q?e.jsx("div",{className:"flex justify-center py-6",children:e.jsx(y,{className:"h-5 w-5 animate-spin"})}):E.length===0?e.jsx("p",{className:"text-sm text-muted-foreground py-2",children:"No paired devices yet."}):e.jsx("div",{className:"divide-y",children:E.map(s=>e.jsxs("div",{className:"flex items-center justify-between py-3",children:[e.jsxs("div",{children:[e.jsxs("div",{className:"font-medium",children:[s.deviceName||`Device ${s.id}`," ",e.jsx(K,{variant:"outline",className:"ml-1",children:s.platform})]}),e.jsxs("div",{className:"text-xs text-muted-foreground",children:["Paired ",new Date(s.createdAt).toLocaleDateString(),s.lastSeenAt&&` · last seen ${new Date(s.lastSeenAt).toLocaleStr
1ing()}`,s.hasPublicKey&&" · hardware key"]})]}),e.jsx(d,{variant:"outline",size:"sm",onClick:()=>R(s.id),children:"Revoke"})]},s.id))}),h.length>0&&e.jsxs("div",{className:"mt-4 text-xs text-muted-foreground",children:[h.length," previously revoked device",h.length>1?"s":"","."]})]})]})]})})}export{z as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.