@samyx/preview-stacks-ui 0.16.0 → 0.18.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 (97) hide show
  1. package/dist/assets/{ActionButton.vue_vue_type_script_setup_true_lang-DjO5RzbR.js → ActionButton.vue_vue_type_script_setup_true_lang-BCYIjIMR.js} +2 -2
  2. package/dist/assets/{ActionButton.vue_vue_type_script_setup_true_lang-DjO5RzbR.js.map → ActionButton.vue_vue_type_script_setup_true_lang-BCYIjIMR.js.map} +1 -1
  3. package/dist/assets/{AxesTab-BZZXKEC0.js → AxesTab-DOL8988q.js} +2 -2
  4. package/dist/assets/{AxesTab-BZZXKEC0.js.map → AxesTab-DOL8988q.js.map} +1 -1
  5. package/dist/assets/{ConfigTab-CFbS48D0.js → ConfigTab-C2kKDMhx.js} +2 -2
  6. package/dist/assets/{ConfigTab-CFbS48D0.js.map → ConfigTab-C2kKDMhx.js.map} +1 -1
  7. package/dist/assets/ConflictNote.vue_vue_type_script_setup_true_lang-DsSczT71.js +2 -0
  8. package/dist/assets/ConflictNote.vue_vue_type_script_setup_true_lang-DsSczT71.js.map +1 -0
  9. package/dist/assets/DangerTab-DGaHMJol.js +2 -0
  10. package/dist/assets/DangerTab-DGaHMJol.js.map +1 -0
  11. package/dist/assets/{DashboardView-CFwb63EX.js → DashboardView-BfF38T9O.js} +2 -2
  12. package/dist/assets/{DashboardView-CFwb63EX.js.map → DashboardView-BfF38T9O.js.map} +1 -1
  13. package/dist/assets/DeploymentDetailView-BGjBFfU4.js +2 -0
  14. package/dist/assets/DeploymentDetailView-BGjBFfU4.js.map +1 -0
  15. package/dist/assets/{DeploymentsView-BX4GOb2A.js → DeploymentsView-BH6-KpUe.js} +2 -2
  16. package/dist/assets/{DeploymentsView-BX4GOb2A.js.map → DeploymentsView-BH6-KpUe.js.map} +1 -1
  17. package/dist/assets/{EquivalentCommand-w1nGowVi.js → EquivalentCommand-BXiEpLtI.js} +2 -2
  18. package/dist/assets/{EquivalentCommand-w1nGowVi.js.map → EquivalentCommand-BXiEpLtI.js.map} +1 -1
  19. package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-D28UQmNR.js → ErrorNote.vue_vue_type_script_setup_true_lang-BxEb711Y.js} +2 -2
  20. package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-D28UQmNR.js.map → ErrorNote.vue_vue_type_script_setup_true_lang-BxEb711Y.js.map} +1 -1
  21. package/dist/assets/{HelpModal.vue_vue_type_script_setup_true_lang-TcgJr2CB.js → HelpModal.vue_vue_type_script_setup_true_lang-BQluOWdQ.js} +2 -2
  22. package/dist/assets/{HelpModal.vue_vue_type_script_setup_true_lang-TcgJr2CB.js.map → HelpModal.vue_vue_type_script_setup_true_lang-BQluOWdQ.js.map} +1 -1
  23. package/dist/assets/JobDetailView-BcE107xE.js +2 -0
  24. package/dist/assets/JobDetailView-BcE107xE.js.map +1 -0
  25. package/dist/assets/JobsView-Dm_Loiq8.js +2 -0
  26. package/dist/assets/JobsView-Dm_Loiq8.js.map +1 -0
  27. package/dist/assets/LogViewer.vue_vue_type_script_setup_true_lang-CJJilqst.js +3 -0
  28. package/dist/assets/LogViewer.vue_vue_type_script_setup_true_lang-CJJilqst.js.map +1 -0
  29. package/dist/assets/{LoginView-1ZpxP8a9.js → LoginView-XoomyWtl.js} +2 -2
  30. package/dist/assets/{LoginView-1ZpxP8a9.js.map → LoginView-XoomyWtl.js.map} +1 -1
  31. package/dist/assets/LogsTab-PetxKgUF.js +5 -0
  32. package/dist/assets/LogsTab-PetxKgUF.js.map +1 -0
  33. package/dist/assets/{NotFoundView-BhCKBrlY.js → NotFoundView-BcVYUxAo.js} +2 -2
  34. package/dist/assets/{NotFoundView-BhCKBrlY.js.map → NotFoundView-BcVYUxAo.js.map} +1 -1
  35. package/dist/assets/{NotifiersView-DN9UiqDs.js → NotifiersView-5xtub30J.js} +2 -2
  36. package/dist/assets/{NotifiersView-DN9UiqDs.js.map → NotifiersView-5xtub30J.js.map} +1 -1
  37. package/dist/assets/OverviewTab-DUEg-y_z.js +2 -0
  38. package/dist/assets/OverviewTab-DUEg-y_z.js.map +1 -0
  39. package/dist/assets/{RegistriesView-CPyfKKWJ.js → RegistriesView-BVFGjxi7.js} +2 -2
  40. package/dist/assets/{RegistriesView-CPyfKKWJ.js.map → RegistriesView-BVFGjxi7.js.map} +1 -1
  41. package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-mzHGsi43.js → RelativeTime.vue_vue_type_script_setup_true_lang-D0buom1m.js} +2 -2
  42. package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-mzHGsi43.js.map → RelativeTime.vue_vue_type_script_setup_true_lang-D0buom1m.js.map} +1 -1
  43. package/dist/assets/{RequiresTab-CjNRorI5.js → RequiresTab-D02oLAp-.js} +2 -2
  44. package/dist/assets/{RequiresTab-CjNRorI5.js.map → RequiresTab-D02oLAp-.js.map} +1 -1
  45. package/dist/assets/{RoutingView-Br16pPwF.js → RoutingView-1bDbTXbH.js} +3 -3
  46. package/dist/assets/{RoutingView-Br16pPwF.js.map → RoutingView-1bDbTXbH.js.map} +1 -1
  47. package/dist/assets/{RuntimeTab-BssqRUYz.js → RuntimeTab-DR6kK7VS.js} +2 -2
  48. package/dist/assets/{RuntimeTab-BssqRUYz.js.map → RuntimeTab-DR6kK7VS.js.map} +1 -1
  49. package/dist/assets/SelectMenu.vue_vue_type_script_setup_true_lang-CoNJMygb.js +2 -0
  50. package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-CLqZAezG.js.map → SelectMenu.vue_vue_type_script_setup_true_lang-CoNJMygb.js.map} +1 -1
  51. package/dist/assets/SettingsView-CNnikDTC.js +2 -0
  52. package/dist/assets/SettingsView-CNnikDTC.js.map +1 -0
  53. package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-CIiWzcE5.js → SkeletonList.vue_vue_type_script_setup_true_lang-BjPZSNt1.js} +2 -2
  54. package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-CIiWzcE5.js.map → SkeletonList.vue_vue_type_script_setup_true_lang-BjPZSNt1.js.map} +1 -1
  55. package/dist/assets/{SpecDetailView-P6Kk7cwx.js → SpecDetailView-CobNJfyA.js} +2 -2
  56. package/dist/assets/{SpecDetailView-P6Kk7cwx.js.map → SpecDetailView-CobNJfyA.js.map} +1 -1
  57. package/dist/assets/{SpecsView-DogEWlyZ.js → SpecsView-CtE6K2qH.js} +2 -2
  58. package/dist/assets/{SpecsView-DogEWlyZ.js.map → SpecsView-CtE6K2qH.js.map} +1 -1
  59. package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-B5C48Wlo.js → StateBadge.vue_vue_type_script_setup_true_lang-CjYFKoV9.js} +2 -2
  60. package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-B5C48Wlo.js.map → StateBadge.vue_vue_type_script_setup_true_lang-CjYFKoV9.js.map} +1 -1
  61. package/dist/assets/SubmitView-BdQ2pmUr.js +50 -0
  62. package/dist/assets/SubmitView-BdQ2pmUr.js.map +1 -0
  63. package/dist/assets/{Teleport-0BmaJsBF.js → Teleport-DXtRNc70.js} +3 -3
  64. package/dist/assets/{Teleport-0BmaJsBF.js.map → Teleport-DXtRNc70.js.map} +1 -1
  65. package/dist/assets/{TerminalTab-DndloIiD.js → TerminalTab-ChiTxP-4.js} +2 -2
  66. package/dist/assets/{TerminalTab-DndloIiD.js.map → TerminalTab-ChiTxP-4.js.map} +1 -1
  67. package/dist/assets/{UsersView-KHRd16sV.js → UsersView-Ci3lY3mC.js} +2 -2
  68. package/dist/assets/{UsersView-KHRd16sV.js.map → UsersView-Ci3lY3mC.js.map} +1 -1
  69. package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-FaBOeld_.js → VarEditor.vue_vue_type_script_setup_true_lang-PNLLh1AH.js} +2 -2
  70. package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-FaBOeld_.js.map → VarEditor.vue_vue_type_script_setup_true_lang-PNLLh1AH.js.map} +1 -1
  71. package/dist/assets/index-WMJXAtiA.css +1 -0
  72. package/dist/assets/index-m4JghVue.js +134 -0
  73. package/dist/assets/index-m4JghVue.js.map +1 -0
  74. package/dist/assets/{useDeployment-DplTFu7s.js → useDeployment-BFsbmcLi.js} +2 -2
  75. package/dist/assets/{useDeployment-DplTFu7s.js.map → useDeployment-BFsbmcLi.js.map} +1 -1
  76. package/dist/index.html +5 -2
  77. package/package.json +1 -1
  78. package/dist/assets/DangerTab-Nyebdr6k.js +0 -2
  79. package/dist/assets/DangerTab-Nyebdr6k.js.map +0 -1
  80. package/dist/assets/DeploymentDetailView-DKCUbiGG.js +0 -2
  81. package/dist/assets/DeploymentDetailView-DKCUbiGG.js.map +0 -1
  82. package/dist/assets/JobDetailView-ow1_d8lc.js +0 -2
  83. package/dist/assets/JobDetailView-ow1_d8lc.js.map +0 -1
  84. package/dist/assets/JobsView-Boi9Cmdc.js +0 -2
  85. package/dist/assets/JobsView-Boi9Cmdc.js.map +0 -1
  86. package/dist/assets/LogsTab-Bgev0F7g.js +0 -4
  87. package/dist/assets/LogsTab-Bgev0F7g.js.map +0 -1
  88. package/dist/assets/OverviewTab-DzZG0moV.js +0 -2
  89. package/dist/assets/OverviewTab-DzZG0moV.js.map +0 -1
  90. package/dist/assets/SelectMenu.vue_vue_type_script_setup_true_lang-CLqZAezG.js +0 -2
  91. package/dist/assets/SettingsView-Cqsv0chw.js +0 -2
  92. package/dist/assets/SettingsView-Cqsv0chw.js.map +0 -1
  93. package/dist/assets/SubmitView-CufYXtWD.js +0 -45
  94. package/dist/assets/SubmitView-CufYXtWD.js.map +0 -1
  95. package/dist/assets/index-BnwKLRM7.css +0 -1
  96. package/dist/assets/index-D8OAewba.js +0 -134
  97. package/dist/assets/index-D8OAewba.js.map +0 -1
@@ -1,3 +1,3 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/xterm-D6Mng-DB.js","assets/_commonjsHelpers-CqkleIqs.js","assets/addon-fit-CevVMrUA.js","assets/xterm-DYP7pi_n.css"])))=>i.map(i=>d[i]);
2
- import{d as q,H as L,K as F,c as v,a as t,f as a,e as C,w as M,_ as W,x as K,g,t as B,r as d,C as T,A as Y,B as j,L as E,M as V,o as c,l as G}from"./index-D8OAewba.js";import{_ as J}from"./ErrorNote.vue_vue_type_script_setup_true_lang-D28UQmNR.js";import{_ as N}from"./SelectMenu.vue_vue_type_script_setup_true_lang-CLqZAezG.js";import{_ as Q}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./Teleport-0BmaJsBF.js";const X={class:"stack"},Z={class:"banner warn"},ee={class:"row wrap",style:{gap:"var(--s3)","align-items":"end"}},ne={class:"field"},oe={class:"field"},te=["disabled"],le={key:2,class:"badge ok"},se={key:1,class:"hint"},ae={key:2,class:"hint"},ie=["data-state"],re=q({__name:"TerminalTab",setup(ue){const O=G(),_=T(()=>String(O.params.id)),P=["sh","bash","ash","zsh","fish"],S=d(null),m=d(""),u=d(""),h=d("sh"),n=d("idle"),f=d(""),p=d(null),w=V(null),U=V(null),y=V(null);let i=null;const I=T(()=>k.value.map(o=>({value:o.name,label:o.name,hint:o.service??void 0}))),k=T(()=>{var o;return((o=S.value)==null?void 0:o.containers.filter(e=>e.state==="running"))??[]});async function R(){var e;const o=await Y.get(`/api/deployments/${encodeURIComponent(_.value)}/runtime`);if(!o.ok){m.value=j(o,"list this deployment's containers");return}m.value="",S.value=o.body,u.value||(u.value=((e=k.value[0])==null?void 0:e.name)??"")}R();async function D(){var A;if(n.value==="connecting"||n.value==="open")return;f.value="",n.value="connecting";const[{Terminal:o},{FitAddon:e}]=await Promise.all([E(()=>import("./xterm-D6Mng-DB.js").then(s=>s.x),__vite__mapDeps([0,1])),E(()=>import("./addon-fit-CevVMrUA.js").then(s=>s.a),__vite__mapDeps([2,1]))]);await E(()=>Promise.resolve({}),__vite__mapDeps([3])),(A=w.value)==null||A.dispose();const l=new o({convertEol:!0,fontSize:13,fontFamily:"var(--font-mono, ui-monospace, monospace)",cursorBlink:!0,theme:{background:"#0b0e14"}}),b=new e;l.loadAddon(b),p.value&&(p.value.innerHTML="",l.open(p.value),b.fit()),w.value=l,U.value=b,i==null||i.disconnect(),i=new ResizeObserver(()=>{try{b.fit()}catch{}}),p.value&&i.observe(p.value);const z=location.protocol==="https:"?"wss":"ws",H=new URLSearchParams({container:u.value,shell:h.value}),r=new WebSocket(`${z}://${location.host}/api/deployments/${encodeURIComponent(_.value)}/terminal?${H}`);r.binaryType="arraybuffer",y.value=r,r.onopen=()=>{n.value="open",l.focus()},r.onmessage=s=>l.write(typeof s.data=="string"?s.data:new Uint8Array(s.data)),r.onclose=s=>{n.value="closed",f.value=s.reason||(s.code===1006?"the connection dropped (the upgrade may have been refused)":`code ${s.code}`)},r.onerror=()=>{n.value==="connecting"&&(f.value="could not open the connection")},l.onData(s=>{r.readyState===WebSocket.OPEN&&r.send(s)})}function x(){var o;(o=y.value)==null||o.close(1e3,"closed by the operator")}function $(){var o,e;i==null||i.disconnect(),i=null,(o=y.value)==null||o.close(),y.value=null,(e=w.value)==null||e.dispose(),w.value=null}return L([u,h],()=>{(n.value==="open"||n.value==="connecting")&&x()}),L(_,()=>{$(),n.value="idle",R()}),F($),(o,e)=>(c(),v("section",X,[t("div",Z,[e[3]||(e[3]=t("b",null,"No TTY.",-1)),e[4]||(e[4]=a(" The shell runs without a terminal device, so there is no prompt, no ",-1)),e[5]||(e[5]=t("code",null,"Ctrl-C",-1)),e[6]||(e[6]=a(", and no full-screen programs (",-1)),e[7]||(e[7]=t("code",null,"top",-1)),e[8]||(e[8]=a(", ",-1)),e[9]||(e[9]=t("code",null,"vim",-1)),e[10]||(e[10]=a("). Type a command and press Enter. ",-1)),C(W,{label:"why"},{default:M(()=>[...e[2]||(e[2]=[a(" A pseudo-terminal needs a wrapper (",-1),t("code",null,"script",-1),a(") whose flags differ across distributions, and it was never verified on a real host — so pstack ships the half it can stand behind. Everything an operator usually opens a shell for (",-1),t("code",null,"ls",-1),a(", ",-1),t("code",null,"cat",-1),a(", ",-1),t("code",null,"env",-1),a(", ",-1),t("code",null,"psql",-1),a(", a migration) works. ",-1)])]),_:1})]),m.value?(c(),K(J,{key:0,text:m.value,title:"Could not list this deployment's containers."},null,8,["text"])):g("",!0),t("div",ee,[t("label",ne,[e[11]||(e[11]=t("span",null,"Container",-1)),C(N,{modelValue:u.value,"onUpdate:modelValue":e[0]||(e[0]=l=>u.value=l),label:"Container",disabled:n.value==="open"||n.value==="connecting",options:I.value},null,8,["modelValue","disabled","options"])]),t("label",oe,[e[12]||(e[12]=t("span",null,"Shell",-1)),C(N,{modelValue:h.value,"onUpdate:modelValue":e[1]||(e[1]=l=>h.value=l),label:"Shell",disabled:n.value==="open"||n.value==="connecting",options:P.map(l=>({value:l,label:l}))},null,8,["modelValue","disabled","options"])]),n.value!=="open"?(c(),v("button",{key:0,class:"primary",disabled:!u.value||n.value==="connecting",onClick:D},B(n.value==="connecting"?"Connecting…":"Open shell"),9,te)):(c(),v("button",{key:1,onClick:x},"Close shell")),e[14]||(e[14]=t("span",{class:"grow"},null,-1)),n.value==="open"?(c(),v("span",le,[...e[13]||(e[13]=[t("span",{class:"dot pulse"},null,-1),a("connected",-1)])])):g("",!0)]),!k.value.length&&!m.value?(c(),v("p",se," Nothing is running for this deployment, so there is nothing to open a shell in. ")):g("",!0),n.value==="closed"&&f.value?(c(),v("p",ae,"Session ended — "+B(f.value)+".",1)):g("",!0),t("div",{ref_key:"host",ref:p,class:"terminal-host","data-state":n.value},null,8,ie),e[15]||(e[15]=t("p",{class:"hint"}," Every session is recorded — who, which container, when — and the record is written when the shell opens, so a session that dies with the process still leaves one. ",-1))]))}}),fe=Q(re,[["__scopeId","data-v-3855f639"]]);export{fe as default};
3
- //# sourceMappingURL=TerminalTab-DndloIiD.js.map
2
+ import{d as q,H as L,K as F,c as v,a as t,f as a,e as C,w as M,_ as W,x as K,g,t as B,r as d,C as T,A as Y,B as j,L as E,M as V,o as c,l as G}from"./index-m4JghVue.js";import{_ as J}from"./ErrorNote.vue_vue_type_script_setup_true_lang-BxEb711Y.js";import{_ as N}from"./SelectMenu.vue_vue_type_script_setup_true_lang-CoNJMygb.js";import{_ as Q}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./Teleport-DXtRNc70.js";const X={class:"stack"},Z={class:"banner warn"},ee={class:"row wrap",style:{gap:"var(--s3)","align-items":"end"}},ne={class:"field"},oe={class:"field"},te=["disabled"],le={key:2,class:"badge ok"},se={key:1,class:"hint"},ae={key:2,class:"hint"},ie=["data-state"],re=q({__name:"TerminalTab",setup(ue){const O=G(),_=T(()=>String(O.params.id)),P=["sh","bash","ash","zsh","fish"],S=d(null),m=d(""),u=d(""),h=d("sh"),n=d("idle"),f=d(""),p=d(null),w=V(null),U=V(null),y=V(null);let i=null;const I=T(()=>k.value.map(o=>({value:o.name,label:o.name,hint:o.service??void 0}))),k=T(()=>{var o;return((o=S.value)==null?void 0:o.containers.filter(e=>e.state==="running"))??[]});async function R(){var e;const o=await Y.get(`/api/deployments/${encodeURIComponent(_.value)}/runtime`);if(!o.ok){m.value=j(o,"list this deployment's containers");return}m.value="",S.value=o.body,u.value||(u.value=((e=k.value[0])==null?void 0:e.name)??"")}R();async function D(){var A;if(n.value==="connecting"||n.value==="open")return;f.value="",n.value="connecting";const[{Terminal:o},{FitAddon:e}]=await Promise.all([E(()=>import("./xterm-D6Mng-DB.js").then(s=>s.x),__vite__mapDeps([0,1])),E(()=>import("./addon-fit-CevVMrUA.js").then(s=>s.a),__vite__mapDeps([2,1]))]);await E(()=>Promise.resolve({}),__vite__mapDeps([3])),(A=w.value)==null||A.dispose();const l=new o({convertEol:!0,fontSize:13,fontFamily:"var(--font-mono, ui-monospace, monospace)",cursorBlink:!0,theme:{background:"#0b0e14"}}),b=new e;l.loadAddon(b),p.value&&(p.value.innerHTML="",l.open(p.value),b.fit()),w.value=l,U.value=b,i==null||i.disconnect(),i=new ResizeObserver(()=>{try{b.fit()}catch{}}),p.value&&i.observe(p.value);const z=location.protocol==="https:"?"wss":"ws",H=new URLSearchParams({container:u.value,shell:h.value}),r=new WebSocket(`${z}://${location.host}/api/deployments/${encodeURIComponent(_.value)}/terminal?${H}`);r.binaryType="arraybuffer",y.value=r,r.onopen=()=>{n.value="open",l.focus()},r.onmessage=s=>l.write(typeof s.data=="string"?s.data:new Uint8Array(s.data)),r.onclose=s=>{n.value="closed",f.value=s.reason||(s.code===1006?"the connection dropped (the upgrade may have been refused)":`code ${s.code}`)},r.onerror=()=>{n.value==="connecting"&&(f.value="could not open the connection")},l.onData(s=>{r.readyState===WebSocket.OPEN&&r.send(s)})}function x(){var o;(o=y.value)==null||o.close(1e3,"closed by the operator")}function $(){var o,e;i==null||i.disconnect(),i=null,(o=y.value)==null||o.close(),y.value=null,(e=w.value)==null||e.dispose(),w.value=null}return L([u,h],()=>{(n.value==="open"||n.value==="connecting")&&x()}),L(_,()=>{$(),n.value="idle",R()}),F($),(o,e)=>(c(),v("section",X,[t("div",Z,[e[3]||(e[3]=t("b",null,"No TTY.",-1)),e[4]||(e[4]=a(" The shell runs without a terminal device, so there is no prompt, no ",-1)),e[5]||(e[5]=t("code",null,"Ctrl-C",-1)),e[6]||(e[6]=a(", and no full-screen programs (",-1)),e[7]||(e[7]=t("code",null,"top",-1)),e[8]||(e[8]=a(", ",-1)),e[9]||(e[9]=t("code",null,"vim",-1)),e[10]||(e[10]=a("). Type a command and press Enter. ",-1)),C(W,{label:"why"},{default:M(()=>[...e[2]||(e[2]=[a(" A pseudo-terminal needs a wrapper (",-1),t("code",null,"script",-1),a(") whose flags differ across distributions, and it was never verified on a real host — so pstack ships the half it can stand behind. Everything an operator usually opens a shell for (",-1),t("code",null,"ls",-1),a(", ",-1),t("code",null,"cat",-1),a(", ",-1),t("code",null,"env",-1),a(", ",-1),t("code",null,"psql",-1),a(", a migration) works. ",-1)])]),_:1})]),m.value?(c(),K(J,{key:0,text:m.value,title:"Could not list this deployment's containers."},null,8,["text"])):g("",!0),t("div",ee,[t("label",ne,[e[11]||(e[11]=t("span",null,"Container",-1)),C(N,{modelValue:u.value,"onUpdate:modelValue":e[0]||(e[0]=l=>u.value=l),label:"Container",disabled:n.value==="open"||n.value==="connecting",options:I.value},null,8,["modelValue","disabled","options"])]),t("label",oe,[e[12]||(e[12]=t("span",null,"Shell",-1)),C(N,{modelValue:h.value,"onUpdate:modelValue":e[1]||(e[1]=l=>h.value=l),label:"Shell",disabled:n.value==="open"||n.value==="connecting",options:P.map(l=>({value:l,label:l}))},null,8,["modelValue","disabled","options"])]),n.value!=="open"?(c(),v("button",{key:0,class:"primary",disabled:!u.value||n.value==="connecting",onClick:D},B(n.value==="connecting"?"Connecting…":"Open shell"),9,te)):(c(),v("button",{key:1,onClick:x},"Close shell")),e[14]||(e[14]=t("span",{class:"grow"},null,-1)),n.value==="open"?(c(),v("span",le,[...e[13]||(e[13]=[t("span",{class:"dot pulse"},null,-1),a("connected",-1)])])):g("",!0)]),!k.value.length&&!m.value?(c(),v("p",se," Nothing is running for this deployment, so there is nothing to open a shell in. ")):g("",!0),n.value==="closed"&&f.value?(c(),v("p",ae,"Session ended — "+B(f.value)+".",1)):g("",!0),t("div",{ref_key:"host",ref:p,class:"terminal-host","data-state":n.value},null,8,ie),e[15]||(e[15]=t("p",{class:"hint"}," Every session is recorded — who, which container, when — and the record is written when the shell opens, so a session that dies with the process still leaves one. ",-1))]))}}),fe=Q(re,[["__scopeId","data-v-3855f639"]]);export{fe as default};
3
+ //# sourceMappingURL=TerminalTab-ChiTxP-4.js.map
@@ -1 +1 @@
1
- {"version":3,"mappings":"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","names":["route","useRoute","id","computed","SHELLS","rt","ref","loadError","container","shell","state","closeReason","host","term","shallowRef","fit","socket","observer","containerOptions","running","c","_a","loadContainers","r","api","problem","connect","Terminal","FitAddon","__vitePreload","n","t","f","proto","qs","ws","e","d","disconnect","teardown","_b","watch","onBeforeUnmount","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_createVNode","InfoHint","_createBlock","ErrorNote","_hoisted_3","_hoisted_4","SelectMenu","$event","_hoisted_5","s","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_toDisplayString"],"ignoreList":[],"sources":["../../src/views/tabs/TerminalTab.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * A shell inside one of this deployment's containers.\n *\n * THE SERVER DECIDES WHICH CONTAINER. This picker is filled from `/runtime`, i.e. the containers the\n * deployment actually owns, and the server checks the name again on upgrade — a hand-typed\n * `?container=pstack-control` is a 404 there, not here. The picker is a convenience, never the\n * boundary.\n *\n * THE COOKIE IS THE CREDENTIAL. A browser cannot set an `Authorization` header on a WebSocket, which\n * is exactly why sessions are cookies (0.10.0) — the handshake is a same-origin GET and carries the\n * session automatically. There is no token in this URL and there must never be one: a URL lands in\n * proxy logs and browser history.\n *\n * NO PTY. The server runs `docker exec -i`, not `-it`, so there is no prompt, no ^C and no `top`.\n * That is stated on the page rather than left for the operator to discover as \"the terminal is\n * broken\" — see the banner below and the comment at the head of `terminal.ts`.\n */\nimport { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue';\nimport { useRoute } from 'vue-router';\nimport { api, problem } from '../../api/client';\nimport type { RuntimeResponse } from '../../api/types';\nimport ErrorNote from '../../components/ErrorNote.vue';\nimport InfoHint from '../../components/InfoHint.vue';\nimport SelectMenu from '../../components/SelectMenu.vue';\n\nconst route = useRoute();\nconst id = computed(() => String(route.params.id));\n\nconst SHELLS = ['sh', 'bash', 'ash', 'zsh', 'fish'];\n\nconst rt = ref<RuntimeResponse | null>(null);\nconst loadError = ref('');\nconst container = ref('');\nconst shell = ref('sh');\nconst state = ref<'idle' | 'connecting' | 'open' | 'closed'>('idle');\nconst closeReason = ref('');\n\nconst host = ref<HTMLDivElement | null>(null);\n/** shallowRef: xterm's Terminal is a large mutable object and must not be made reactive. */\nconst term = shallowRef<import('@xterm/xterm').Terminal | null>(null);\nconst fit = shallowRef<import('@xterm/addon-fit').FitAddon | null>(null);\nconst socket = shallowRef<WebSocket | null>(null);\nlet observer: ResizeObserver | null = null;\n\nconst containerOptions = computed(() =>\n running.value.map((c) => ({ value: c.name, label: c.name, hint: c.service ?? undefined })),\n);\n\nconst running = computed(() => rt.value?.containers.filter((c) => c.state === 'running') ?? []);\n\nasync function loadContainers() {\n const r = await api.get<RuntimeResponse>(\n `/api/deployments/${encodeURIComponent(id.value)}/runtime`,\n );\n if (!r.ok) {\n loadError.value = problem(r, \"list this deployment's containers\");\n return;\n }\n loadError.value = '';\n rt.value = r.body;\n if (!container.value) container.value = running.value[0]?.name ?? '';\n}\nvoid loadContainers();\n\nasync function connect() {\n if (state.value === 'connecting' || state.value === 'open') return;\n closeReason.value = '';\n state.value = 'connecting';\n\n // Loaded on demand, not in the app shell: xterm is ~250KB and only this tab needs it.\n const [{ Terminal }, { FitAddon }] = await Promise.all([\n import('@xterm/xterm'),\n import('@xterm/addon-fit'),\n ]);\n await import('@xterm/xterm/css/xterm.css');\n\n term.value?.dispose();\n const t = new Terminal({\n // The OUTPUT half of the line discipline the missing pty would have provided. A real terminal\n // converts NL to CR-NL on the way out (ONLCR); without it every `\\n` moves DOWN a row without\n // returning the carriage, and `ls` renders as a staircase marching off the right edge. The input\n // half — CR to NL — is done server-side; see `crToNl` in api.ts.\n convertEol: true,\n fontSize: 13,\n fontFamily: 'var(--font-mono, ui-monospace, monospace)',\n cursorBlink: true,\n theme: { background: '#0b0e14' },\n });\n const f = new FitAddon();\n t.loadAddon(f);\n if (host.value) {\n host.value.innerHTML = '';\n t.open(host.value);\n f.fit();\n }\n term.value = t;\n fit.value = f;\n\n observer?.disconnect();\n observer = new ResizeObserver(() => {\n try {\n f.fit();\n } catch {\n /* the element is detached mid-transition */\n }\n });\n if (host.value) observer.observe(host.value);\n\n const proto = location.protocol === 'https:' ? 'wss' : 'ws';\n const qs = new URLSearchParams({ container: container.value, shell: shell.value });\n const ws = new WebSocket(\n `${proto}://${location.host}/api/deployments/${encodeURIComponent(id.value)}/terminal?${qs}`,\n );\n ws.binaryType = 'arraybuffer';\n socket.value = ws;\n\n ws.onopen = () => {\n state.value = 'open';\n t.focus();\n };\n // Bytes, not text: decoding per frame would corrupt a multi-byte character split across two\n // reads. xterm takes a Uint8Array and handles the boundary itself.\n ws.onmessage = (e) =>\n t.write(typeof e.data === 'string' ? e.data : new Uint8Array(e.data as ArrayBuffer));\n ws.onclose = (e) => {\n state.value = 'closed';\n // A close CODE alone tells an operator nothing. 1006 in particular is the browser's \"it just\n // went away\", which here almost always means the upgrade was refused.\n closeReason.value =\n e.reason || (e.code === 1006 ? 'the connection dropped (the upgrade may have been refused)' : `code ${e.code}`);\n };\n ws.onerror = () => {\n if (state.value === 'connecting') closeReason.value = 'could not open the connection';\n };\n t.onData((d) => {\n if (ws.readyState === WebSocket.OPEN) ws.send(d);\n });\n}\n\nfunction disconnect() {\n socket.value?.close(1000, 'closed by the operator');\n}\n\nfunction teardown() {\n observer?.disconnect();\n observer = null;\n socket.value?.close();\n socket.value = null;\n term.value?.dispose();\n term.value = null;\n}\n\n// Switching container or shell must not leave the old shell running against a dead view.\nwatch([container, shell], () => {\n if (state.value === 'open' || state.value === 'connecting') disconnect();\n});\nwatch(id, () => {\n teardown();\n state.value = 'idle';\n void loadContainers();\n});\nonBeforeUnmount(teardown);\n</script>\n\n<template>\n <section class=\"stack\">\n <div class=\"banner warn\">\n <b>No TTY.</b> The shell runs without a terminal device, so there is no prompt, no\n <code>Ctrl-C</code>, and no full-screen programs (<code>top</code>, <code>vim</code>). Type a\n command and press Enter.\n <InfoHint label=\"why\">\n A pseudo-terminal needs a wrapper (<code>script</code>) whose flags differ across\n distributions, and it was never verified on a real host — so pstack ships the half it can\n stand behind. Everything an operator usually opens a shell for (<code>ls</code>,\n <code>cat</code>, <code>env</code>, <code>psql</code>, a migration) works.\n </InfoHint>\n </div>\n\n <ErrorNote v-if=\"loadError\" :text=\"loadError\" title=\"Could not list this deployment's containers.\" />\n\n <div class=\"row wrap\" style=\"gap: var(--s3); align-items: end\">\n <label class=\"field\">\n <span>Container</span>\n <SelectMenu v-model=\"container\" label=\"Container\" :disabled=\"state === 'open' || state === 'connecting'\" :options=\"containerOptions\" />\n </label>\n <label class=\"field\">\n <span>Shell</span>\n <SelectMenu v-model=\"shell\" label=\"Shell\" :disabled=\"state === 'open' || state === 'connecting'\" :options=\"SHELLS.map((s) => ({ value: s, label: s }))\" />\n </label>\n <button\n v-if=\"state !== 'open'\"\n class=\"primary\"\n :disabled=\"!container || state === 'connecting'\"\n @click=\"connect\"\n >\n {{ state === 'connecting' ? 'Connecting…' : 'Open shell' }}\n </button>\n <button v-else @click=\"disconnect\">Close shell</button>\n <span class=\"grow\" />\n <span v-if=\"state === 'open'\" class=\"badge ok\"><span class=\"dot pulse\" />connected</span>\n </div>\n\n <p v-if=\"!running.length && !loadError\" class=\"hint\">\n Nothing is running for this deployment, so there is nothing to open a shell in.\n </p>\n\n <p v-if=\"state === 'closed' && closeReason\" class=\"hint\">Session ended — {{ closeReason }}.</p>\n\n <div ref=\"host\" class=\"terminal-host\" :data-state=\"state\" />\n\n <p class=\"hint\">\n Every session is recorded — who, which container, when — and the record is written when the\n shell opens, so a session that dies with the process still leaves one.\n </p>\n </section>\n</template>\n\n<style scoped>\n.terminal-host {\n min-height: 26rem;\n border: 1px solid var(--border);\n border-radius: var(--r3);\n background: #0b0e14;\n padding: var(--s2);\n overflow: hidden;\n}\n.terminal-host[data-state='idle'] {\n display: none;\n}\n</style>\n"],"file":"assets/TerminalTab-DndloIiD.js"}
1
+ {"version":3,"mappings":"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","names":["route","useRoute","id","computed","SHELLS","rt","ref","loadError","container","shell","state","closeReason","host","term","shallowRef","fit","socket","observer","containerOptions","running","c","_a","loadContainers","r","api","problem","connect","Terminal","FitAddon","__vitePreload","n","t","f","proto","qs","ws","e","d","disconnect","teardown","_b","watch","onBeforeUnmount","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_createVNode","InfoHint","_createBlock","ErrorNote","_hoisted_3","_hoisted_4","SelectMenu","$event","_hoisted_5","s","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_toDisplayString"],"ignoreList":[],"sources":["../../src/views/tabs/TerminalTab.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * A shell inside one of this deployment's containers.\n *\n * THE SERVER DECIDES WHICH CONTAINER. This picker is filled from `/runtime`, i.e. the containers the\n * deployment actually owns, and the server checks the name again on upgrade — a hand-typed\n * `?container=pstack-control` is a 404 there, not here. The picker is a convenience, never the\n * boundary.\n *\n * THE COOKIE IS THE CREDENTIAL. A browser cannot set an `Authorization` header on a WebSocket, which\n * is exactly why sessions are cookies (0.10.0) — the handshake is a same-origin GET and carries the\n * session automatically. There is no token in this URL and there must never be one: a URL lands in\n * proxy logs and browser history.\n *\n * NO PTY. The server runs `docker exec -i`, not `-it`, so there is no prompt, no ^C and no `top`.\n * That is stated on the page rather than left for the operator to discover as \"the terminal is\n * broken\" — see the banner below and the comment at the head of `terminal.ts`.\n */\nimport { computed, onBeforeUnmount, ref, shallowRef, watch } from 'vue';\nimport { useRoute } from 'vue-router';\nimport { api, problem } from '../../api/client';\nimport type { RuntimeResponse } from '../../api/types';\nimport ErrorNote from '../../components/ErrorNote.vue';\nimport InfoHint from '../../components/InfoHint.vue';\nimport SelectMenu from '../../components/SelectMenu.vue';\n\nconst route = useRoute();\nconst id = computed(() => String(route.params.id));\n\nconst SHELLS = ['sh', 'bash', 'ash', 'zsh', 'fish'];\n\nconst rt = ref<RuntimeResponse | null>(null);\nconst loadError = ref('');\nconst container = ref('');\nconst shell = ref('sh');\nconst state = ref<'idle' | 'connecting' | 'open' | 'closed'>('idle');\nconst closeReason = ref('');\n\nconst host = ref<HTMLDivElement | null>(null);\n/** shallowRef: xterm's Terminal is a large mutable object and must not be made reactive. */\nconst term = shallowRef<import('@xterm/xterm').Terminal | null>(null);\nconst fit = shallowRef<import('@xterm/addon-fit').FitAddon | null>(null);\nconst socket = shallowRef<WebSocket | null>(null);\nlet observer: ResizeObserver | null = null;\n\nconst containerOptions = computed(() =>\n running.value.map((c) => ({ value: c.name, label: c.name, hint: c.service ?? undefined })),\n);\n\nconst running = computed(() => rt.value?.containers.filter((c) => c.state === 'running') ?? []);\n\nasync function loadContainers() {\n const r = await api.get<RuntimeResponse>(\n `/api/deployments/${encodeURIComponent(id.value)}/runtime`,\n );\n if (!r.ok) {\n loadError.value = problem(r, \"list this deployment's containers\");\n return;\n }\n loadError.value = '';\n rt.value = r.body;\n if (!container.value) container.value = running.value[0]?.name ?? '';\n}\nvoid loadContainers();\n\nasync function connect() {\n if (state.value === 'connecting' || state.value === 'open') return;\n closeReason.value = '';\n state.value = 'connecting';\n\n // Loaded on demand, not in the app shell: xterm is ~250KB and only this tab needs it.\n const [{ Terminal }, { FitAddon }] = await Promise.all([\n import('@xterm/xterm'),\n import('@xterm/addon-fit'),\n ]);\n await import('@xterm/xterm/css/xterm.css');\n\n term.value?.dispose();\n const t = new Terminal({\n // The OUTPUT half of the line discipline the missing pty would have provided. A real terminal\n // converts NL to CR-NL on the way out (ONLCR); without it every `\\n` moves DOWN a row without\n // returning the carriage, and `ls` renders as a staircase marching off the right edge. The input\n // half — CR to NL — is done server-side; see `crToNl` in api.ts.\n convertEol: true,\n fontSize: 13,\n fontFamily: 'var(--font-mono, ui-monospace, monospace)',\n cursorBlink: true,\n theme: { background: '#0b0e14' },\n });\n const f = new FitAddon();\n t.loadAddon(f);\n if (host.value) {\n host.value.innerHTML = '';\n t.open(host.value);\n f.fit();\n }\n term.value = t;\n fit.value = f;\n\n observer?.disconnect();\n observer = new ResizeObserver(() => {\n try {\n f.fit();\n } catch {\n /* the element is detached mid-transition */\n }\n });\n if (host.value) observer.observe(host.value);\n\n const proto = location.protocol === 'https:' ? 'wss' : 'ws';\n const qs = new URLSearchParams({ container: container.value, shell: shell.value });\n const ws = new WebSocket(\n `${proto}://${location.host}/api/deployments/${encodeURIComponent(id.value)}/terminal?${qs}`,\n );\n ws.binaryType = 'arraybuffer';\n socket.value = ws;\n\n ws.onopen = () => {\n state.value = 'open';\n t.focus();\n };\n // Bytes, not text: decoding per frame would corrupt a multi-byte character split across two\n // reads. xterm takes a Uint8Array and handles the boundary itself.\n ws.onmessage = (e) =>\n t.write(typeof e.data === 'string' ? e.data : new Uint8Array(e.data as ArrayBuffer));\n ws.onclose = (e) => {\n state.value = 'closed';\n // A close CODE alone tells an operator nothing. 1006 in particular is the browser's \"it just\n // went away\", which here almost always means the upgrade was refused.\n closeReason.value =\n e.reason || (e.code === 1006 ? 'the connection dropped (the upgrade may have been refused)' : `code ${e.code}`);\n };\n ws.onerror = () => {\n if (state.value === 'connecting') closeReason.value = 'could not open the connection';\n };\n t.onData((d) => {\n if (ws.readyState === WebSocket.OPEN) ws.send(d);\n });\n}\n\nfunction disconnect() {\n socket.value?.close(1000, 'closed by the operator');\n}\n\nfunction teardown() {\n observer?.disconnect();\n observer = null;\n socket.value?.close();\n socket.value = null;\n term.value?.dispose();\n term.value = null;\n}\n\n// Switching container or shell must not leave the old shell running against a dead view.\nwatch([container, shell], () => {\n if (state.value === 'open' || state.value === 'connecting') disconnect();\n});\nwatch(id, () => {\n teardown();\n state.value = 'idle';\n void loadContainers();\n});\nonBeforeUnmount(teardown);\n</script>\n\n<template>\n <section class=\"stack\">\n <div class=\"banner warn\">\n <b>No TTY.</b> The shell runs without a terminal device, so there is no prompt, no\n <code>Ctrl-C</code>, and no full-screen programs (<code>top</code>, <code>vim</code>). Type a\n command and press Enter.\n <InfoHint label=\"why\">\n A pseudo-terminal needs a wrapper (<code>script</code>) whose flags differ across\n distributions, and it was never verified on a real host — so pstack ships the half it can\n stand behind. Everything an operator usually opens a shell for (<code>ls</code>,\n <code>cat</code>, <code>env</code>, <code>psql</code>, a migration) works.\n </InfoHint>\n </div>\n\n <ErrorNote v-if=\"loadError\" :text=\"loadError\" title=\"Could not list this deployment's containers.\" />\n\n <div class=\"row wrap\" style=\"gap: var(--s3); align-items: end\">\n <label class=\"field\">\n <span>Container</span>\n <SelectMenu v-model=\"container\" label=\"Container\" :disabled=\"state === 'open' || state === 'connecting'\" :options=\"containerOptions\" />\n </label>\n <label class=\"field\">\n <span>Shell</span>\n <SelectMenu v-model=\"shell\" label=\"Shell\" :disabled=\"state === 'open' || state === 'connecting'\" :options=\"SHELLS.map((s) => ({ value: s, label: s }))\" />\n </label>\n <button\n v-if=\"state !== 'open'\"\n class=\"primary\"\n :disabled=\"!container || state === 'connecting'\"\n @click=\"connect\"\n >\n {{ state === 'connecting' ? 'Connecting…' : 'Open shell' }}\n </button>\n <button v-else @click=\"disconnect\">Close shell</button>\n <span class=\"grow\" />\n <span v-if=\"state === 'open'\" class=\"badge ok\"><span class=\"dot pulse\" />connected</span>\n </div>\n\n <p v-if=\"!running.length && !loadError\" class=\"hint\">\n Nothing is running for this deployment, so there is nothing to open a shell in.\n </p>\n\n <p v-if=\"state === 'closed' && closeReason\" class=\"hint\">Session ended — {{ closeReason }}.</p>\n\n <div ref=\"host\" class=\"terminal-host\" :data-state=\"state\" />\n\n <p class=\"hint\">\n Every session is recorded — who, which container, when — and the record is written when the\n shell opens, so a session that dies with the process still leaves one.\n </p>\n </section>\n</template>\n\n<style scoped>\n.terminal-host {\n min-height: 26rem;\n border: 1px solid var(--border);\n border-radius: var(--r3);\n background: #0b0e14;\n padding: var(--s2);\n overflow: hidden;\n}\n.terminal-host[data-state='idle'] {\n display: none;\n}\n</style>\n"],"file":"assets/TerminalTab-ChiTxP-4.js"}
@@ -1,2 +1,2 @@
1
- import{d as z,c as n,a as e,f as y,e as c,w as b,_ as S,x as C,g as _,t as i,F as R,s as I,h as P,i as A,v as T,u as U,b as g,T as G,r as d,C as H,A as m,B as f,o as a,N as x}from"./index-D8OAewba.js";import{_ as B}from"./ActionButton.vue_vue_type_script_setup_true_lang-DjO5RzbR.js";import{E as D}from"./EquivalentCommand-w1nGowVi.js";import{_ as J}from"./ErrorNote.vue_vue_type_script_setup_true_lang-D28UQmNR.js";import{_ as E}from"./RelativeTime.vue_vue_type_script_setup_true_lang-mzHGsi43.js";import{_ as F}from"./SkeletonList.vue_vue_type_script_setup_true_lang-CIiWzcE5.js";import{_ as K}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./useFormat-CKJxQp_i.js";const Q={class:"page-head"},X={class:"sub"},Z={key:1,class:"banner ok"},ee={class:"code"},se={class:"row"},te={class:"grid-2"},oe={class:"panel"},ae={class:"phead"},ne={class:"mute",style:{"font-size":"var(--t-xs)"}},le={key:1,class:"cards"},re={class:"name","data-label":"user"},ie={key:0,class:"badge info"},de={class:"dim nowrap","data-label":"added"},ue={class:"right nowrap","data-label":""},ce=["onClick"],pe={key:2,class:"hint"},ve={class:"row wrap two-up"},me={class:"field grow"},he={class:"field grow"},we={class:"row"},ke=["disabled"],fe={class:"panel"},ye={class:"phead"},be={key:1,class:"cards"},_e={class:"name","data-label":"name"},ge={class:"dim nowrap","data-label":"last used"},$e={key:1,class:"mute"},Ce={class:"right nowrap","data-label":""},Ae={key:2,class:"hint"},Te={class:"field"},Ue={class:"row"},xe=["disabled"],Ve={class:"sheet",role:"dialog","aria-modal":"true","aria-label":"Change password"},Ne={class:"hint"},Pe={class:"field"},Se={class:"row"},Re=["disabled"],Ie=z({__name:"UsersView",setup(Be){const $=d([]),V=d([]),N=d(!1),r=d(""),l=d({username:"",password:""}),p=d(""),u=d(null),h=d(""),w=d(null),L=H(()=>l.value.username.trim().length>0&&l.value.password.length>=8);async function k(){const[o,s]=await Promise.all([m.get("/api/users"),m.get("/api/tokens")]);if(N.value=!0,!o.ok){r.value=f(o,"load the accounts");return}r.value="",$.value=o.body.users??[],V.value=s.ok?s.body.tokens??[]:[]}k();async function M(){const o=await m.post("/api/users",{username:l.value.username.trim(),password:l.value.password});if(!o.ok){r.value=f(o,"create this account");return}x("ok",`Created ${o.body.user.username}.`),l.value={username:"",password:""},k()}async function O(o){const s=await m.del(`/api/users/${o.id}`);if(!s.ok){r.value=f(s,`delete ${o.username}`);return}x("ok",`Deleted ${o.username}.`),k()}async function W(){var t;const o=u.value;if(!o)return;const s=await m.put(`/api/users/${o.id}/password`,{password:h.value});if(!s.ok){r.value=f(s,`change the password for ${o.username}`);return}u.value=null,h.value="",x("ok",o.id===((t=g.user)==null?void 0:t.id)?"Password changed. You have been signed out everywhere — sign in again.":`Password changed. ${o.username} has been signed out everywhere.`),k()}async function Y(){const o=await m.post("/api/tokens",{name:p.value.trim()});if(!o.ok){r.value=f(o,"create this token");return}w.value={what:`Personal token “${p.value.trim()}”`,value:o.body.token,note:"Send it as a bearer token. The server stores only a hash, so this is the only time it is shown."},p.value="",k()}async function j(o){const s=await m.del(`/api/tokens/${o.id}`);if(!s.ok){r.value=f(s,`revoke ${o.name}`);return}x("ok",`Revoked ${o.name}.`),k()}return(o,s)=>(a(),n("div",null,[e("div",Q,[e("div",null,[s[9]||(s[9]=e("h1",null,"Users & access",-1)),e("div",X,[s[8]||(s[8]=y(" Who can sign in, and the tokens they use from scripts ",-1)),c(S,{label:"how access works here"},{default:b(()=>[...s[7]||(s[7]=[y(" Three kinds of caller. The host token from the environment is the root credential CI holds. An account signs in with a password and gets a browser session. A personal token belongs to one account and is what a script uses — it can do anything that account can. ",-1)])]),_:1})])])]),r.value?(a(),C(J,{key:0,text:r.value,title:"Something went wrong."},null,8,["text"])):_("",!0),w.value?(a(),n("div",Z,[e("b",null,i(w.value.what)+" — copy it now.",1),e("p",null,i(w.value.note),1),e("pre",ee,i(w.value.value),1),e("div",se,[e("button",{onClick:s[0]||(s[0]=t=>w.value=null)},"I have stored it")])])):_("",!0),e("div",te,[e("section",oe,[e("div",ae,[s[10]||(s[10]=e("h2",{class:"section"},"Accounts",-1)),s[11]||(s[11]=e("span",{class:"grow"},null,-1)),e("span",ne,i($.value.length),1)]),N.value?$.value.length?(a(),n("table",le,[s[13]||(s[13]=e("thead",null,[e("tr",null,[e("th",null,"User"),e("th",null,"Added"),e("th",{"aria-label":"Actions"})])],-1)),e("tbody",null,[(a(!0),n(R,null,I($.value,t=>{var v;return a(),n("tr",{key:t.id},[e("td",re,[e("b",null,i(t.username),1),t.id===((v=U(g).user)==null?void 0:v.id)?(a(),n("span",ie,"you")):_("",!0)]),e("td",de,[c(E,{at:t.createdAt},null,8,["at"])]),e("td",ue,[e("button",{class:"ghost sm",onClick:q=>{u.value=t,h.value=""}},"Change password",8,ce),c(B,{class:"danger sm",confirm:`Delete ${t.username}? Their sessions and tokens go too.`,onRun:q=>O(t)},{default:b(()=>[...s[12]||(s[12]=[y("Delete",-1)])]),_:1},8,["confirm","onRun"])])])}),128))])])):(a(),n("p",pe,"No accounts yet.")):(a(),C(F,{key:0,rows:3})),e("form",{class:"stack-form",onSubmit:P(M,["prevent"])},[s[17]||(s[17]=e("h3",null,"Add an account",-1)),e("div",ve,[e("label",me,[s[14]||(s[14]=e("span",null,"Username",-1)),A(e("input",{"onUpdate:modelValue":s[1]||(s[1]=t=>l.value.username=t),type:"text",autocomplete:"off"},null,512),[[T,l.value.username]])]),e("label",he,[s[15]||(s[15]=e("span",null,"Password",-1)),A(e("input",{"onUpdate:modelValue":s[2]||(s[2]=t=>l.value.password=t),type:"password",autocomplete:"new-password"},null,512),[[T,l.value.password]])])]),s[18]||(s[18]=e("p",{class:"hint"},"At least 8 characters.",-1)),e("div",we,[e("button",{class:"primary",type:"submit",disabled:!L.value},"Add account",8,ke),s[16]||(s[16]=e("span",{class:"grow"},null,-1))]),c(D,{what:"this account creation",method:"POST",path:"/api/users",body:{username:l.value.username||"alice",password:"…"}},null,8,["body"])],32)]),e("section",fe,[e("div",ye,[s[20]||(s[20]=e("h2",{class:"section"},"Your API tokens",-1)),c(S,{label:"when to use one"},{default:b(()=>[...s[19]||(s[19]=[y(" A script or a CI job signing in as you. It carries whatever you can do, so give each one a name you will recognise later and revoke the ones you stop using. ",-1)])]),_:1})]),N.value?V.value.length?(a(),n("table",be,[s[22]||(s[22]=e("thead",null,[e("tr",null,[e("th",null,"Name"),e("th",null,"Last used"),e("th",{"aria-label":"Actions"})])],-1)),e("tbody",null,[(a(!0),n(R,null,I(V.value,t=>(a(),n("tr",{key:t.id},[e("td",_e,[e("b",null,i(t.name),1)]),e("td",ge,[t.lastUsedAt?(a(),C(E,{key:0,at:t.lastUsedAt},null,8,["at"])):(a(),n("span",$e,"never"))]),e("td",Ce,[c(B,{class:"danger sm",confirm:`Revoke ${t.name}? Anything using it stops working.`,onRun:v=>j(t)},{default:b(()=>[...s[21]||(s[21]=[y("Revoke",-1)])]),_:1},8,["confirm","onRun"])])]))),128))])])):(a(),n("p",Ae,i(U(g).root?"The host token is not an account, so it has no personal tokens.":"No tokens yet."),1)):(a(),C(F,{key:0,rows:2})),U(g).root?_("",!0):(a(),n("form",{key:3,class:"stack-form",onSubmit:P(Y,["prevent"])},[s[24]||(s[24]=e("h3",null,"New token",-1)),e("label",Te,[s[23]||(s[23]=e("span",null,"What is it for",-1)),A(e("input",{"onUpdate:modelValue":s[3]||(s[3]=t=>p.value=t),type:"text",placeholder:"ci-deploy",autocomplete:"off"},null,512),[[T,p.value]])]),e("div",Ue,[e("button",{class:"primary",type:"submit",disabled:!p.value.trim()},"Create token",8,xe)]),c(D,{what:"this token creation",method:"POST",path:"/api/tokens",body:{name:p.value||"ci-deploy"}},null,8,["body"])],32))])]),c(G,{name:"fade"},{default:b(()=>{var t;return[u.value?(a(),n("div",{key:0,class:"scrim",onClick:s[6]||(s[6]=P(v=>u.value=null,["self"]))},[e("div",Ve,[e("h3",null,"Change password for "+i(u.value.username),1),e("p",Ne," This signs "+i(u.value.id===((t=U(g).user)==null?void 0:t.id)?"you":"them")+" out everywhere — every session and personal token is revoked. ",1),e("label",Pe,[s[25]||(s[25]=e("span",null,"New password",-1)),A(e("input",{"onUpdate:modelValue":s[4]||(s[4]=v=>h.value=v),type:"password",autocomplete:"new-password"},null,512),[[T,h.value]])]),e("div",Se,[e("button",{class:"primary",disabled:h.value.length<8,onClick:W}," Change password ",8,Re),e("button",{class:"ghost",onClick:s[5]||(s[5]=v=>u.value=null)},"Cancel")])])])):_("",!0)]}),_:1})]))}}),je=K(Ie,[["__scopeId","data-v-09c4a1c4"]]);export{je as default};
2
- //# sourceMappingURL=UsersView-KHRd16sV.js.map
1
+ import{d as z,c as n,a as e,f as y,e as c,w as b,_ as S,x as C,g as _,t as i,F as R,s as I,h as P,i as A,v as T,u as U,b as g,T as G,r as d,C as H,A as m,B as f,o as a,N as x}from"./index-m4JghVue.js";import{_ as B}from"./ActionButton.vue_vue_type_script_setup_true_lang-BCYIjIMR.js";import{E as D}from"./EquivalentCommand-BXiEpLtI.js";import{_ as J}from"./ErrorNote.vue_vue_type_script_setup_true_lang-BxEb711Y.js";import{_ as E}from"./RelativeTime.vue_vue_type_script_setup_true_lang-D0buom1m.js";import{_ as F}from"./SkeletonList.vue_vue_type_script_setup_true_lang-BjPZSNt1.js";import{_ as K}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./useFormat-CKJxQp_i.js";const Q={class:"page-head"},X={class:"sub"},Z={key:1,class:"banner ok"},ee={class:"code"},se={class:"row"},te={class:"grid-2"},oe={class:"panel"},ae={class:"phead"},ne={class:"mute",style:{"font-size":"var(--t-xs)"}},le={key:1,class:"cards"},re={class:"name","data-label":"user"},ie={key:0,class:"badge info"},de={class:"dim nowrap","data-label":"added"},ue={class:"right nowrap","data-label":""},ce=["onClick"],pe={key:2,class:"hint"},ve={class:"row wrap two-up"},me={class:"field grow"},he={class:"field grow"},we={class:"row"},ke=["disabled"],fe={class:"panel"},ye={class:"phead"},be={key:1,class:"cards"},_e={class:"name","data-label":"name"},ge={class:"dim nowrap","data-label":"last used"},$e={key:1,class:"mute"},Ce={class:"right nowrap","data-label":""},Ae={key:2,class:"hint"},Te={class:"field"},Ue={class:"row"},xe=["disabled"],Ve={class:"sheet",role:"dialog","aria-modal":"true","aria-label":"Change password"},Ne={class:"hint"},Pe={class:"field"},Se={class:"row"},Re=["disabled"],Ie=z({__name:"UsersView",setup(Be){const $=d([]),V=d([]),N=d(!1),r=d(""),l=d({username:"",password:""}),p=d(""),u=d(null),h=d(""),w=d(null),L=H(()=>l.value.username.trim().length>0&&l.value.password.length>=8);async function k(){const[o,s]=await Promise.all([m.get("/api/users"),m.get("/api/tokens")]);if(N.value=!0,!o.ok){r.value=f(o,"load the accounts");return}r.value="",$.value=o.body.users??[],V.value=s.ok?s.body.tokens??[]:[]}k();async function M(){const o=await m.post("/api/users",{username:l.value.username.trim(),password:l.value.password});if(!o.ok){r.value=f(o,"create this account");return}x("ok",`Created ${o.body.user.username}.`),l.value={username:"",password:""},k()}async function O(o){const s=await m.del(`/api/users/${o.id}`);if(!s.ok){r.value=f(s,`delete ${o.username}`);return}x("ok",`Deleted ${o.username}.`),k()}async function W(){var t;const o=u.value;if(!o)return;const s=await m.put(`/api/users/${o.id}/password`,{password:h.value});if(!s.ok){r.value=f(s,`change the password for ${o.username}`);return}u.value=null,h.value="",x("ok",o.id===((t=g.user)==null?void 0:t.id)?"Password changed. You have been signed out everywhere — sign in again.":`Password changed. ${o.username} has been signed out everywhere.`),k()}async function Y(){const o=await m.post("/api/tokens",{name:p.value.trim()});if(!o.ok){r.value=f(o,"create this token");return}w.value={what:`Personal token “${p.value.trim()}”`,value:o.body.token,note:"Send it as a bearer token. The server stores only a hash, so this is the only time it is shown."},p.value="",k()}async function j(o){const s=await m.del(`/api/tokens/${o.id}`);if(!s.ok){r.value=f(s,`revoke ${o.name}`);return}x("ok",`Revoked ${o.name}.`),k()}return(o,s)=>(a(),n("div",null,[e("div",Q,[e("div",null,[s[9]||(s[9]=e("h1",null,"Users & access",-1)),e("div",X,[s[8]||(s[8]=y(" Who can sign in, and the tokens they use from scripts ",-1)),c(S,{label:"how access works here"},{default:b(()=>[...s[7]||(s[7]=[y(" Three kinds of caller. The host token from the environment is the root credential CI holds. An account signs in with a password and gets a browser session. A personal token belongs to one account and is what a script uses — it can do anything that account can. ",-1)])]),_:1})])])]),r.value?(a(),C(J,{key:0,text:r.value,title:"Something went wrong."},null,8,["text"])):_("",!0),w.value?(a(),n("div",Z,[e("b",null,i(w.value.what)+" — copy it now.",1),e("p",null,i(w.value.note),1),e("pre",ee,i(w.value.value),1),e("div",se,[e("button",{onClick:s[0]||(s[0]=t=>w.value=null)},"I have stored it")])])):_("",!0),e("div",te,[e("section",oe,[e("div",ae,[s[10]||(s[10]=e("h2",{class:"section"},"Accounts",-1)),s[11]||(s[11]=e("span",{class:"grow"},null,-1)),e("span",ne,i($.value.length),1)]),N.value?$.value.length?(a(),n("table",le,[s[13]||(s[13]=e("thead",null,[e("tr",null,[e("th",null,"User"),e("th",null,"Added"),e("th",{"aria-label":"Actions"})])],-1)),e("tbody",null,[(a(!0),n(R,null,I($.value,t=>{var v;return a(),n("tr",{key:t.id},[e("td",re,[e("b",null,i(t.username),1),t.id===((v=U(g).user)==null?void 0:v.id)?(a(),n("span",ie,"you")):_("",!0)]),e("td",de,[c(E,{at:t.createdAt},null,8,["at"])]),e("td",ue,[e("button",{class:"ghost sm",onClick:q=>{u.value=t,h.value=""}},"Change password",8,ce),c(B,{class:"danger sm",confirm:`Delete ${t.username}? Their sessions and tokens go too.`,onRun:q=>O(t)},{default:b(()=>[...s[12]||(s[12]=[y("Delete",-1)])]),_:1},8,["confirm","onRun"])])])}),128))])])):(a(),n("p",pe,"No accounts yet.")):(a(),C(F,{key:0,rows:3})),e("form",{class:"stack-form",onSubmit:P(M,["prevent"])},[s[17]||(s[17]=e("h3",null,"Add an account",-1)),e("div",ve,[e("label",me,[s[14]||(s[14]=e("span",null,"Username",-1)),A(e("input",{"onUpdate:modelValue":s[1]||(s[1]=t=>l.value.username=t),type:"text",autocomplete:"off"},null,512),[[T,l.value.username]])]),e("label",he,[s[15]||(s[15]=e("span",null,"Password",-1)),A(e("input",{"onUpdate:modelValue":s[2]||(s[2]=t=>l.value.password=t),type:"password",autocomplete:"new-password"},null,512),[[T,l.value.password]])])]),s[18]||(s[18]=e("p",{class:"hint"},"At least 8 characters.",-1)),e("div",we,[e("button",{class:"primary",type:"submit",disabled:!L.value},"Add account",8,ke),s[16]||(s[16]=e("span",{class:"grow"},null,-1))]),c(D,{what:"this account creation",method:"POST",path:"/api/users",body:{username:l.value.username||"alice",password:"…"}},null,8,["body"])],32)]),e("section",fe,[e("div",ye,[s[20]||(s[20]=e("h2",{class:"section"},"Your API tokens",-1)),c(S,{label:"when to use one"},{default:b(()=>[...s[19]||(s[19]=[y(" A script or a CI job signing in as you. It carries whatever you can do, so give each one a name you will recognise later and revoke the ones you stop using. ",-1)])]),_:1})]),N.value?V.value.length?(a(),n("table",be,[s[22]||(s[22]=e("thead",null,[e("tr",null,[e("th",null,"Name"),e("th",null,"Last used"),e("th",{"aria-label":"Actions"})])],-1)),e("tbody",null,[(a(!0),n(R,null,I(V.value,t=>(a(),n("tr",{key:t.id},[e("td",_e,[e("b",null,i(t.name),1)]),e("td",ge,[t.lastUsedAt?(a(),C(E,{key:0,at:t.lastUsedAt},null,8,["at"])):(a(),n("span",$e,"never"))]),e("td",Ce,[c(B,{class:"danger sm",confirm:`Revoke ${t.name}? Anything using it stops working.`,onRun:v=>j(t)},{default:b(()=>[...s[21]||(s[21]=[y("Revoke",-1)])]),_:1},8,["confirm","onRun"])])]))),128))])])):(a(),n("p",Ae,i(U(g).root?"The host token is not an account, so it has no personal tokens.":"No tokens yet."),1)):(a(),C(F,{key:0,rows:2})),U(g).root?_("",!0):(a(),n("form",{key:3,class:"stack-form",onSubmit:P(Y,["prevent"])},[s[24]||(s[24]=e("h3",null,"New token",-1)),e("label",Te,[s[23]||(s[23]=e("span",null,"What is it for",-1)),A(e("input",{"onUpdate:modelValue":s[3]||(s[3]=t=>p.value=t),type:"text",placeholder:"ci-deploy",autocomplete:"off"},null,512),[[T,p.value]])]),e("div",Ue,[e("button",{class:"primary",type:"submit",disabled:!p.value.trim()},"Create token",8,xe)]),c(D,{what:"this token creation",method:"POST",path:"/api/tokens",body:{name:p.value||"ci-deploy"}},null,8,["body"])],32))])]),c(G,{name:"fade"},{default:b(()=>{var t;return[u.value?(a(),n("div",{key:0,class:"scrim",onClick:s[6]||(s[6]=P(v=>u.value=null,["self"]))},[e("div",Ve,[e("h3",null,"Change password for "+i(u.value.username),1),e("p",Ne," This signs "+i(u.value.id===((t=U(g).user)==null?void 0:t.id)?"you":"them")+" out everywhere — every session and personal token is revoked. ",1),e("label",Pe,[s[25]||(s[25]=e("span",null,"New password",-1)),A(e("input",{"onUpdate:modelValue":s[4]||(s[4]=v=>h.value=v),type:"password",autocomplete:"new-password"},null,512),[[T,h.value]])]),e("div",Se,[e("button",{class:"primary",disabled:h.value.length<8,onClick:W}," Change password ",8,Re),e("button",{class:"ghost",onClick:s[5]||(s[5]=v=>u.value=null)},"Cancel")])])])):_("",!0)]}),_:1})]))}}),je=K(Ie,[["__scopeId","data-v-09c4a1c4"]]);export{je as default};
2
+ //# sourceMappingURL=UsersView-Ci3lY3mC.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"UsersView-KHRd16sV.js","sources":["../../src/views/UsersView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Accounts and personal API tokens.\n *\n * Accounts landed in 0.10.0 with no UI at all, so the only way to add a second operator was a curl\n * call — which is exactly the thing a control plane exists to spare people.\n *\n * TWO AUDIENCES, TWO SECTIONS, and the split is deliberate. **Accounts** are about other people and\n * are shared state; **tokens** are yours alone (the API scopes them to the caller and cannot show\n * anyone else's). Mixing them into one table would imply an admin can manage another person's\n * tokens, which is not true and would be a bad idea if it were.\n *\n * SECRETS ARE SHOWN ONCE, and the page says so in the same breath as showing them. Same discipline\n * as a notifier signing secret and a registry credential: there is no read path on the server, so\n * this is not a policy that could be relaxed later — the value genuinely stops existing.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport { authState } from '../composables/useAuth';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport InfoHint from '../components/InfoHint.vue';\nimport RelativeTime from '../components/RelativeTime.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\n\ntype User = { id: number; username: string; role: string; createdAt: number };\ntype Token = { id: number; name: string; createdAt: number; lastUsedAt: number | null };\n\nconst users = ref<User[]>([]);\nconst tokens = ref<Token[]>([]);\nconst loaded = ref(false);\nconst listError = ref('');\n\nconst newUser = ref({ username: '', password: '' });\nconst newToken = ref('');\nconst pwFor = ref<User | null>(null);\nconst pwValue = ref('');\n\n/** Held only until dismissed — there is no way to show either of these again. */\nconst revealed = ref<{ what: string; value: string; note: string } | null>(null);\n\nconst canCreateUser = computed(\n () => newUser.value.username.trim().length > 0 && newUser.value.password.length >= 8,\n);\n\nasync function load(): Promise<void> {\n const [u, t] = await Promise.all([\n api.get<{ users: User[] }>('/api/users'),\n api.get<{ tokens: Token[] }>('/api/tokens'),\n ]);\n loaded.value = true;\n if (!u.ok) {\n listError.value = problem(u, 'load the accounts');\n return;\n }\n listError.value = '';\n users.value = u.body.users ?? [];\n // Root (PSTACK_TOKEN) is not a user, so it has no personal tokens — an error here is expected.\n tokens.value = t.ok ? (t.body.tokens ?? []) : [];\n}\nvoid load();\n\nasync function createUser(): Promise<void> {\n const r = await api.post<{ user: User }>('/api/users', {\n username: newUser.value.username.trim(),\n password: newUser.value.password,\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this account');\n return;\n }\n toast('ok', `Created ${r.body.user.username}.`);\n newUser.value = { username: '', password: '' };\n void load();\n}\n\nasync function removeUser(u: User): Promise<void> {\n const r = await api.del(`/api/users/${u.id}`);\n // The server refuses to delete the last account. That is a rule worth surfacing verbatim rather\n // than as \"could not delete\" — it tells the operator what to do instead.\n if (!r.ok) {\n listError.value = problem(r, `delete ${u.username}`);\n return;\n }\n toast('ok', `Deleted ${u.username}.`);\n void load();\n}\n\nasync function changePassword(): Promise<void> {\n const u = pwFor.value;\n if (!u) return;\n const r = await api.put(`/api/users/${u.id}/password`, { password: pwValue.value });\n if (!r.ok) {\n listError.value = problem(r, `change the password for ${u.username}`);\n return;\n }\n pwFor.value = null;\n pwValue.value = '';\n // Not a footnote: the change signs that user out everywhere, and if it was YOU, the next request\n // is a 401. Saying so here is the difference between an expected step and an apparent bug.\n toast(\n 'ok',\n u.id === authState.user?.id\n ? `Password changed. You have been signed out everywhere — sign in again.`\n : `Password changed. ${u.username} has been signed out everywhere.`,\n );\n void load();\n}\n\nasync function createToken(): Promise<void> {\n const r = await api.post<{ token: string; id: number }>('/api/tokens', {\n name: newToken.value.trim(),\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this token');\n return;\n }\n revealed.value = {\n what: `Personal token “${newToken.value.trim()}”`,\n value: r.body.token,\n note: 'Send it as a bearer token. The server stores only a hash, so this is the only time it is shown.',\n };\n newToken.value = '';\n void load();\n}\n\nasync function removeToken(t: Token): Promise<void> {\n const r = await api.del(`/api/tokens/${t.id}`);\n if (!r.ok) {\n listError.value = problem(r, `revoke ${t.name}`);\n return;\n }\n toast('ok', `Revoked ${t.name}.`);\n void load();\n}\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>Users &amp; access</h1>\n <div class=\"sub\">\n Who can sign in, and the tokens they use from scripts\n <InfoHint label=\"how access works here\">\n Three kinds of caller. The host token from the environment is the root credential CI\n holds. An account signs in with a password and gets a browser session. A personal token\n belongs to one account and is what a script uses — it can do anything that account can.\n </InfoHint>\n </div>\n </div>\n </div>\n\n <ErrorNote v-if=\"listError\" :text=\"listError\" title=\"Something went wrong.\" />\n\n <div v-if=\"revealed\" class=\"banner ok\">\n <b>{{ revealed.what }} — copy it now.</b>\n <p>{{ revealed.note }}</p>\n <pre class=\"code\">{{ revealed.value }}</pre>\n <div class=\"row\">\n <button @click=\"revealed = null\">I have stored it</button>\n </div>\n </div>\n\n <div class=\"grid-2\">\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Accounts</h2>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ users.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"3\" />\n <!-- `cards` + `data-label` per cell: below 640px the same markup renders as cards. Without\n it the row's two buttons pushed the table past the viewport and got clipped. -->\n <table v-else-if=\"users.length\" class=\"cards\">\n <thead>\n <tr>\n <th>User</th>\n <th>Added</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"u in users\" :key=\"u.id\">\n <td class=\"name\" data-label=\"user\">\n <b>{{ u.username }}</b>\n <span v-if=\"u.id === authState.user?.id\" class=\"badge info\">you</span>\n </td>\n <td class=\"dim nowrap\" data-label=\"added\"><RelativeTime :at=\"u.createdAt\" /></td>\n <td class=\"right nowrap\" data-label=\"\">\n <button class=\"ghost sm\" @click=\"pwFor = u; pwValue = ''\">Change password</button>\n <ActionButton\n class=\"danger sm\"\n :confirm=\"`Delete ${u.username}? Their sessions and tokens go too.`\"\n @run=\"removeUser(u)\"\n >Delete</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No accounts yet.</p>\n\n <form class=\"stack-form\" @submit.prevent=\"createUser\">\n <h3>Add an account</h3>\n <div class=\"row wrap two-up\">\n <label class=\"field grow\">\n <span>Username</span>\n <input v-model=\"newUser.username\" type=\"text\" autocomplete=\"off\" />\n </label>\n <label class=\"field grow\">\n <span>Password</span>\n <input v-model=\"newUser.password\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n </div>\n <p class=\"hint\">At least 8 characters.</p>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!canCreateUser\">Add account</button>\n <span class=\"grow\" />\n </div>\n <EquivalentCommand\n what=\"this account creation\"\n method=\"POST\"\n path=\"/api/users\"\n :body=\"{ username: newUser.username || 'alice', password: '…' }\"\n />\n </form>\n </section>\n\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Your API tokens</h2>\n <InfoHint label=\"when to use one\">\n A script or a CI job signing in as you. It carries whatever you can do, so give each one\n a name you will recognise later and revoke the ones you stop using.\n </InfoHint>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" />\n <table v-else-if=\"tokens.length\" class=\"cards\">\n <thead>\n <tr>\n <th>Name</th>\n <th>Last used</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"t in tokens\" :key=\"t.id\">\n <td class=\"name\" data-label=\"name\"><b>{{ t.name }}</b></td>\n <td class=\"dim nowrap\" data-label=\"last used\">\n <RelativeTime v-if=\"t.lastUsedAt\" :at=\"t.lastUsedAt\" />\n <span v-else class=\"mute\">never</span>\n </td>\n <td class=\"right nowrap\" data-label=\"\">\n <ActionButton\n class=\"danger sm\"\n :confirm=\"`Revoke ${t.name}? Anything using it stops working.`\"\n @run=\"removeToken(t)\"\n >Revoke</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">\n {{ authState.root ? 'The host token is not an account, so it has no personal tokens.' : 'No tokens yet.' }}\n </p>\n\n <form v-if=\"!authState.root\" class=\"stack-form\" @submit.prevent=\"createToken\">\n <h3>New token</h3>\n <label class=\"field\">\n <span>What is it for</span>\n <input v-model=\"newToken\" type=\"text\" placeholder=\"ci-deploy\" autocomplete=\"off\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!newToken.trim()\">Create token</button>\n </div>\n <EquivalentCommand\n what=\"this token creation\"\n method=\"POST\"\n path=\"/api/tokens\"\n :body=\"{ name: newToken || 'ci-deploy' }\"\n />\n </form>\n </section>\n </div>\n\n <!-- Password change: a dialog, because it is a distinct decision about one row. -->\n <Transition name=\"fade\">\n <div v-if=\"pwFor\" class=\"scrim\" @click.self=\"pwFor = null\">\n <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Change password\">\n <h3>Change password for {{ pwFor.username }}</h3>\n <p class=\"hint\">\n This signs {{ pwFor.id === authState.user?.id ? 'you' : 'them' }} out everywhere —\n every session and personal token is revoked.\n </p>\n <label class=\"field\">\n <span>New password</span>\n <input v-model=\"pwValue\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" :disabled=\"pwValue.length < 8\" @click=\"changePassword\">\n Change password\n </button>\n <button class=\"ghost\" @click=\"pwFor = null\">Cancel</button>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n</template>\n\n<style scoped>\n.stack-form {\n margin-top: var(--s5);\n padding-top: var(--s4);\n border-top: 1px solid var(--line-soft);\n display: flex;\n flex-direction: column;\n gap: var(--s3);\n}\n.stack-form h3 {\n font-size: var(--t-sm);\n color: var(--fg-dim);\n}\ntd.right {\n text-align: end;\n}\n/* Two fields share a line where there is room and stack where there is not — at 390px a pair of\n * half-width inputs is two inputs nobody can type into. */\n.two-up > .field {\n flex: 1 1 200px;\n}\ntd.right button {\n margin-inline-start: var(--s2);\n}\n</style>\n"],"names":["users","ref","tokens","loaded","listError","newUser","newToken","pwFor","pwValue","revealed","canCreateUser","computed","load","u","t","api","problem","createUser","r","toast","removeUser","changePassword","_a","authState","createToken","removeToken","_createElementBlock","_createElementVNode","_hoisted_1","_cache","_hoisted_2","_createVNode","InfoHint","_createBlock","ErrorNote","_openBlock","_hoisted_3","_toDisplayString","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_Fragment","_renderList","_hoisted_11","_unref","_hoisted_12","_hoisted_13","RelativeTime","_hoisted_14","$event","_hoisted_15","ActionButton","_hoisted_16","SkeletonList","_hoisted_17","_hoisted_18","_vModelText","_hoisted_19","_hoisted_20","_hoisted_21","EquivalentCommand","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","_hoisted_31","_hoisted_32","_Transition","_hoisted_33","_hoisted_34","_hoisted_35","_hoisted_36","_hoisted_37"],"mappings":"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"}
1
+ {"version":3,"file":"UsersView-Ci3lY3mC.js","sources":["../../src/views/UsersView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Accounts and personal API tokens.\n *\n * Accounts landed in 0.10.0 with no UI at all, so the only way to add a second operator was a curl\n * call — which is exactly the thing a control plane exists to spare people.\n *\n * TWO AUDIENCES, TWO SECTIONS, and the split is deliberate. **Accounts** are about other people and\n * are shared state; **tokens** are yours alone (the API scopes them to the caller and cannot show\n * anyone else's). Mixing them into one table would imply an admin can manage another person's\n * tokens, which is not true and would be a bad idea if it were.\n *\n * SECRETS ARE SHOWN ONCE, and the page says so in the same breath as showing them. Same discipline\n * as a notifier signing secret and a registry credential: there is no read path on the server, so\n * this is not a policy that could be relaxed later — the value genuinely stops existing.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport { authState } from '../composables/useAuth';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport InfoHint from '../components/InfoHint.vue';\nimport RelativeTime from '../components/RelativeTime.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\n\ntype User = { id: number; username: string; role: string; createdAt: number };\ntype Token = { id: number; name: string; createdAt: number; lastUsedAt: number | null };\n\nconst users = ref<User[]>([]);\nconst tokens = ref<Token[]>([]);\nconst loaded = ref(false);\nconst listError = ref('');\n\nconst newUser = ref({ username: '', password: '' });\nconst newToken = ref('');\nconst pwFor = ref<User | null>(null);\nconst pwValue = ref('');\n\n/** Held only until dismissed — there is no way to show either of these again. */\nconst revealed = ref<{ what: string; value: string; note: string } | null>(null);\n\nconst canCreateUser = computed(\n () => newUser.value.username.trim().length > 0 && newUser.value.password.length >= 8,\n);\n\nasync function load(): Promise<void> {\n const [u, t] = await Promise.all([\n api.get<{ users: User[] }>('/api/users'),\n api.get<{ tokens: Token[] }>('/api/tokens'),\n ]);\n loaded.value = true;\n if (!u.ok) {\n listError.value = problem(u, 'load the accounts');\n return;\n }\n listError.value = '';\n users.value = u.body.users ?? [];\n // Root (PSTACK_TOKEN) is not a user, so it has no personal tokens — an error here is expected.\n tokens.value = t.ok ? (t.body.tokens ?? []) : [];\n}\nvoid load();\n\nasync function createUser(): Promise<void> {\n const r = await api.post<{ user: User }>('/api/users', {\n username: newUser.value.username.trim(),\n password: newUser.value.password,\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this account');\n return;\n }\n toast('ok', `Created ${r.body.user.username}.`);\n newUser.value = { username: '', password: '' };\n void load();\n}\n\nasync function removeUser(u: User): Promise<void> {\n const r = await api.del(`/api/users/${u.id}`);\n // The server refuses to delete the last account. That is a rule worth surfacing verbatim rather\n // than as \"could not delete\" — it tells the operator what to do instead.\n if (!r.ok) {\n listError.value = problem(r, `delete ${u.username}`);\n return;\n }\n toast('ok', `Deleted ${u.username}.`);\n void load();\n}\n\nasync function changePassword(): Promise<void> {\n const u = pwFor.value;\n if (!u) return;\n const r = await api.put(`/api/users/${u.id}/password`, { password: pwValue.value });\n if (!r.ok) {\n listError.value = problem(r, `change the password for ${u.username}`);\n return;\n }\n pwFor.value = null;\n pwValue.value = '';\n // Not a footnote: the change signs that user out everywhere, and if it was YOU, the next request\n // is a 401. Saying so here is the difference between an expected step and an apparent bug.\n toast(\n 'ok',\n u.id === authState.user?.id\n ? `Password changed. You have been signed out everywhere — sign in again.`\n : `Password changed. ${u.username} has been signed out everywhere.`,\n );\n void load();\n}\n\nasync function createToken(): Promise<void> {\n const r = await api.post<{ token: string; id: number }>('/api/tokens', {\n name: newToken.value.trim(),\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this token');\n return;\n }\n revealed.value = {\n what: `Personal token “${newToken.value.trim()}”`,\n value: r.body.token,\n note: 'Send it as a bearer token. The server stores only a hash, so this is the only time it is shown.',\n };\n newToken.value = '';\n void load();\n}\n\nasync function removeToken(t: Token): Promise<void> {\n const r = await api.del(`/api/tokens/${t.id}`);\n if (!r.ok) {\n listError.value = problem(r, `revoke ${t.name}`);\n return;\n }\n toast('ok', `Revoked ${t.name}.`);\n void load();\n}\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>Users &amp; access</h1>\n <div class=\"sub\">\n Who can sign in, and the tokens they use from scripts\n <InfoHint label=\"how access works here\">\n Three kinds of caller. The host token from the environment is the root credential CI\n holds. An account signs in with a password and gets a browser session. A personal token\n belongs to one account and is what a script uses — it can do anything that account can.\n </InfoHint>\n </div>\n </div>\n </div>\n\n <ErrorNote v-if=\"listError\" :text=\"listError\" title=\"Something went wrong.\" />\n\n <div v-if=\"revealed\" class=\"banner ok\">\n <b>{{ revealed.what }} — copy it now.</b>\n <p>{{ revealed.note }}</p>\n <pre class=\"code\">{{ revealed.value }}</pre>\n <div class=\"row\">\n <button @click=\"revealed = null\">I have stored it</button>\n </div>\n </div>\n\n <div class=\"grid-2\">\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Accounts</h2>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ users.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"3\" />\n <!-- `cards` + `data-label` per cell: below 640px the same markup renders as cards. Without\n it the row's two buttons pushed the table past the viewport and got clipped. -->\n <table v-else-if=\"users.length\" class=\"cards\">\n <thead>\n <tr>\n <th>User</th>\n <th>Added</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"u in users\" :key=\"u.id\">\n <td class=\"name\" data-label=\"user\">\n <b>{{ u.username }}</b>\n <span v-if=\"u.id === authState.user?.id\" class=\"badge info\">you</span>\n </td>\n <td class=\"dim nowrap\" data-label=\"added\"><RelativeTime :at=\"u.createdAt\" /></td>\n <td class=\"right nowrap\" data-label=\"\">\n <button class=\"ghost sm\" @click=\"pwFor = u; pwValue = ''\">Change password</button>\n <ActionButton\n class=\"danger sm\"\n :confirm=\"`Delete ${u.username}? Their sessions and tokens go too.`\"\n @run=\"removeUser(u)\"\n >Delete</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No accounts yet.</p>\n\n <form class=\"stack-form\" @submit.prevent=\"createUser\">\n <h3>Add an account</h3>\n <div class=\"row wrap two-up\">\n <label class=\"field grow\">\n <span>Username</span>\n <input v-model=\"newUser.username\" type=\"text\" autocomplete=\"off\" />\n </label>\n <label class=\"field grow\">\n <span>Password</span>\n <input v-model=\"newUser.password\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n </div>\n <p class=\"hint\">At least 8 characters.</p>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!canCreateUser\">Add account</button>\n <span class=\"grow\" />\n </div>\n <EquivalentCommand\n what=\"this account creation\"\n method=\"POST\"\n path=\"/api/users\"\n :body=\"{ username: newUser.username || 'alice', password: '…' }\"\n />\n </form>\n </section>\n\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Your API tokens</h2>\n <InfoHint label=\"when to use one\">\n A script or a CI job signing in as you. It carries whatever you can do, so give each one\n a name you will recognise later and revoke the ones you stop using.\n </InfoHint>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" />\n <table v-else-if=\"tokens.length\" class=\"cards\">\n <thead>\n <tr>\n <th>Name</th>\n <th>Last used</th>\n <th aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody>\n <tr v-for=\"t in tokens\" :key=\"t.id\">\n <td class=\"name\" data-label=\"name\"><b>{{ t.name }}</b></td>\n <td class=\"dim nowrap\" data-label=\"last used\">\n <RelativeTime v-if=\"t.lastUsedAt\" :at=\"t.lastUsedAt\" />\n <span v-else class=\"mute\">never</span>\n </td>\n <td class=\"right nowrap\" data-label=\"\">\n <ActionButton\n class=\"danger sm\"\n :confirm=\"`Revoke ${t.name}? Anything using it stops working.`\"\n @run=\"removeToken(t)\"\n >Revoke</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">\n {{ authState.root ? 'The host token is not an account, so it has no personal tokens.' : 'No tokens yet.' }}\n </p>\n\n <form v-if=\"!authState.root\" class=\"stack-form\" @submit.prevent=\"createToken\">\n <h3>New token</h3>\n <label class=\"field\">\n <span>What is it for</span>\n <input v-model=\"newToken\" type=\"text\" placeholder=\"ci-deploy\" autocomplete=\"off\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!newToken.trim()\">Create token</button>\n </div>\n <EquivalentCommand\n what=\"this token creation\"\n method=\"POST\"\n path=\"/api/tokens\"\n :body=\"{ name: newToken || 'ci-deploy' }\"\n />\n </form>\n </section>\n </div>\n\n <!-- Password change: a dialog, because it is a distinct decision about one row. -->\n <Transition name=\"fade\">\n <div v-if=\"pwFor\" class=\"scrim\" @click.self=\"pwFor = null\">\n <div class=\"sheet\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Change password\">\n <h3>Change password for {{ pwFor.username }}</h3>\n <p class=\"hint\">\n This signs {{ pwFor.id === authState.user?.id ? 'you' : 'them' }} out everywhere —\n every session and personal token is revoked.\n </p>\n <label class=\"field\">\n <span>New password</span>\n <input v-model=\"pwValue\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" :disabled=\"pwValue.length < 8\" @click=\"changePassword\">\n Change password\n </button>\n <button class=\"ghost\" @click=\"pwFor = null\">Cancel</button>\n </div>\n </div>\n </div>\n </Transition>\n </div>\n</template>\n\n<style scoped>\n.stack-form {\n margin-top: var(--s5);\n padding-top: var(--s4);\n border-top: 1px solid var(--line-soft);\n display: flex;\n flex-direction: column;\n gap: var(--s3);\n}\n.stack-form h3 {\n font-size: var(--t-sm);\n color: var(--fg-dim);\n}\ntd.right {\n text-align: end;\n}\n/* Two fields share a line where there is room and stack where there is not — at 390px a pair of\n * half-width inputs is two inputs nobody can type into. */\n.two-up > .field {\n flex: 1 1 200px;\n}\ntd.right button {\n margin-inline-start: var(--s2);\n}\n</style>\n"],"names":["users","ref","tokens","loaded","listError","newUser","newToken","pwFor","pwValue","revealed","canCreateUser","computed","load","u","t","api","problem","createUser","r","toast","removeUser","changePassword","_a","authState","createToken","removeToken","_createElementBlock","_createElementVNode","_hoisted_1","_cache","_hoisted_2","_createVNode","InfoHint","_createBlock","ErrorNote","_openBlock","_hoisted_3","_toDisplayString","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_hoisted_9","_hoisted_10","_Fragment","_renderList","_hoisted_11","_unref","_hoisted_12","_hoisted_13","RelativeTime","_hoisted_14","$event","_hoisted_15","ActionButton","_hoisted_16","SkeletonList","_hoisted_17","_hoisted_18","_vModelText","_hoisted_19","_hoisted_20","_hoisted_21","EquivalentCommand","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","_hoisted_31","_hoisted_32","_Transition","_hoisted_33","_hoisted_34","_hoisted_35","_hoisted_36","_hoisted_37"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import{d as k,c as r,a,F as f,s as h,o as c}from"./index-D8OAewba.js";const b=["value","aria-label","onInput"],V=["value","aria-label","onInput"],_=["aria-label","onClick"],x=k({__name:"VarEditor",props:{modelValue:{}},emits:["update:modelValue","change"],setup(d,{emit:p}){const s=d,t=p;function m(u,l){const o=s.modelValue.map((e,n)=>n===u?{...e,...l}:e);t("update:modelValue",o),t("change")}function i(){t("update:modelValue",[...s.modelValue,{k:"",v:""}]),t("change")}function v(u){const l=s.modelValue.filter((o,e)=>e!==u);t("update:modelValue",l.length?l:[{k:"",v:""}]),t("change")}return(u,l)=>(c(),r("div",null,[l[0]||(l[0]=a("div",{class:"kv head"},[a("span",null,"name"),a("span",null,"value"),a("span")],-1)),(c(!0),r(f,null,h(d.modelValue,(o,e)=>(c(),r("div",{key:e,class:"kv"},[a("input",{type:"text",value:o.k,"aria-label":`variable name ${e+1}`,placeholder:"PR",spellcheck:"false",autocomplete:"off",onInput:n=>m(e,{k:n.target.value.trim()})},null,40,b),a("input",{type:"text",value:o.v,"aria-label":`variable value ${e+1}`,placeholder:"7",spellcheck:"false",autocomplete:"off",onInput:n=>m(e,{v:n.target.value})},null,40,V),a("button",{class:"sm ghost","aria-label":`remove variable ${e+1}`,onClick:n=>v(e)},"−",8,_)]))),128)),a("button",{class:"sm",onClick:i},"+ variable")]))}});export{x as _};
2
- //# sourceMappingURL=VarEditor.vue_vue_type_script_setup_true_lang-FaBOeld_.js.map
1
+ import{d as k,c as r,a,F as f,s as h,o as c}from"./index-m4JghVue.js";const b=["value","aria-label","onInput"],V=["value","aria-label","onInput"],_=["aria-label","onClick"],x=k({__name:"VarEditor",props:{modelValue:{}},emits:["update:modelValue","change"],setup(d,{emit:p}){const s=d,t=p;function m(u,l){const o=s.modelValue.map((e,n)=>n===u?{...e,...l}:e);t("update:modelValue",o),t("change")}function i(){t("update:modelValue",[...s.modelValue,{k:"",v:""}]),t("change")}function v(u){const l=s.modelValue.filter((o,e)=>e!==u);t("update:modelValue",l.length?l:[{k:"",v:""}]),t("change")}return(u,l)=>(c(),r("div",null,[l[0]||(l[0]=a("div",{class:"kv head"},[a("span",null,"name"),a("span",null,"value"),a("span")],-1)),(c(!0),r(f,null,h(d.modelValue,(o,e)=>(c(),r("div",{key:e,class:"kv"},[a("input",{type:"text",value:o.k,"aria-label":`variable name ${e+1}`,placeholder:"PR",spellcheck:"false",autocomplete:"off",onInput:n=>m(e,{k:n.target.value.trim()})},null,40,b),a("input",{type:"text",value:o.v,"aria-label":`variable value ${e+1}`,placeholder:"7",spellcheck:"false",autocomplete:"off",onInput:n=>m(e,{v:n.target.value})},null,40,V),a("button",{class:"sm ghost","aria-label":`remove variable ${e+1}`,onClick:n=>v(e)},"−",8,_)]))),128)),a("button",{class:"sm",onClick:i},"+ variable")]))}});export{x as _};
2
+ //# sourceMappingURL=VarEditor.vue_vue_type_script_setup_true_lang-PNLLh1AH.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"VarEditor.vue_vue_type_script_setup_true_lang-FaBOeld_.js","sources":["../../src/components/VarEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The free-form variable editor, shared by the Config tab and Submit.\n *\n * Free-form on purpose: the spec's `env:` block lists what it DECLARES, which is not the same as\n * what it NEEDS — `stack: pr-${PR}` consumes `PR` without declaring it anywhere. The authoritative\n * list of what is missing is the server's 400 text, not this form.\n */\nimport type { VarPair } from '../api/types';\n\nconst props = defineProps<{ modelValue: VarPair[] }>();\nconst emit = defineEmits<{ 'update:modelValue': [VarPair[]]; change: [] }>();\n\nfunction set(i: number, patch: Partial<VarPair>): void {\n const next = props.modelValue.map((e, n) => (n === i ? { ...e, ...patch } : e));\n emit('update:modelValue', next);\n emit('change');\n}\n\nfunction add(): void {\n emit('update:modelValue', [...props.modelValue, { k: '', v: '' }]);\n emit('change');\n}\n\nfunction remove(i: number): void {\n const next = props.modelValue.filter((_, n) => n !== i);\n // Never leave the editor with no rows: an empty list looks like a broken control.\n emit('update:modelValue', next.length ? next : [{ k: '', v: '' }]);\n emit('change');\n}\n</script>\n\n<template>\n <div>\n <div class=\"kv head\"><span>name</span><span>value</span><span /></div>\n <div v-for=\"(e, i) in modelValue\" :key=\"i\" class=\"kv\">\n <input\n type=\"text\"\n :value=\"e.k\"\n :aria-label=\"`variable name ${i + 1}`\"\n placeholder=\"PR\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n @input=\"set(i, { k: ($event.target as HTMLInputElement).value.trim() })\"\n />\n <input\n type=\"text\"\n :value=\"e.v\"\n :aria-label=\"`variable value ${i + 1}`\"\n placeholder=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n @input=\"set(i, { v: ($event.target as HTMLInputElement).value })\"\n />\n <button class=\"sm ghost\" :aria-label=\"`remove variable ${i + 1}`\" @click=\"remove(i)\">−</button>\n </div>\n <button class=\"sm\" @click=\"add\">+ variable</button>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","set","i","patch","next","add","remove","_","n","_createElementBlock","_createElementVNode","_openBlock","_Fragment","_renderList","e","$event","_hoisted_3"],"mappings":"kRAUA,MAAMA,EAAQC,EACRC,EAAOC,EAEb,SAASC,EAAIC,EAAWC,EAA+B,CACrD,MAAMC,EAAOP,EAAM,WAAW,IAAI,CAAC,EAAG,IAAO,IAAMK,EAAI,CAAE,GAAG,EAAG,GAAGC,CAAA,EAAU,CAAE,EAC9EJ,EAAK,oBAAqBK,CAAI,EAC9BL,EAAK,QAAQ,CACf,CAEA,SAASM,GAAY,CACnBN,EAAK,oBAAqB,CAAC,GAAGF,EAAM,WAAY,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEE,EAAK,QAAQ,CACf,CAEA,SAASO,EAAOJ,EAAiB,CAC/B,MAAME,EAAOP,EAAM,WAAW,OAAO,CAACU,EAAGC,IAAMA,IAAMN,CAAC,EAEtDH,EAAK,oBAAqBK,EAAK,OAASA,EAAO,CAAC,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEL,EAAK,QAAQ,CACf,mBAIEU,EAwBM,MAAA,KAAA,aAvBJC,EAAsE,MAAA,CAAjE,MAAM,WAAS,CAACA,EAAiB,YAAX,MAAI,EAAOA,EAAkB,YAAZ,OAAK,EAAOA,EAAQ,MAAA,CAAA,QAChEC,EAAA,EAAA,EAAAF,EAoBMG,EAAA,KAAAC,EApBgBf,EAAA,WAAU,CAAnBgB,EAAGZ,SAAhBO,EAoBM,MAAA,CApB6B,IAAKP,EAAG,MAAM,IAAA,GAC/CQ,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,8BAA6BZ,EAAC,CAAA,GAC/B,YAAY,KACZ,WAAW,QACX,aAAa,MACZ,QAAKa,GAAEd,EAAIC,EAAC,CAAA,EAAQa,EAAO,OAA4B,MAAM,MAAI,CAAA,CAAA,aAEpEL,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,+BAA8BZ,EAAC,CAAA,GAChC,YAAY,IACZ,WAAW,QACX,aAAa,MACZ,QAAKa,GAAEd,EAAIC,KAASa,EAAO,OAA4B,KAAA,CAAK,CAAA,aAE/DL,EAA+F,SAAA,CAAvF,MAAM,WAAY,gCAA+BR,EAAC,CAAA,GAAS,QAAKa,GAAET,EAAOJ,CAAC,CAAA,EAAG,IAAC,EAAAc,CAAA,CAAA,WAExFN,EAAmD,SAAA,CAA3C,MAAM,KAAM,QAAOL,CAAA,EAAK,YAAU,CAAA"}
1
+ {"version":3,"file":"VarEditor.vue_vue_type_script_setup_true_lang-PNLLh1AH.js","sources":["../../src/components/VarEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The free-form variable editor, shared by the Config tab and Submit.\n *\n * Free-form on purpose: the spec's `env:` block lists what it DECLARES, which is not the same as\n * what it NEEDS — `stack: pr-${PR}` consumes `PR` without declaring it anywhere. The authoritative\n * list of what is missing is the server's 400 text, not this form.\n */\nimport type { VarPair } from '../api/types';\n\nconst props = defineProps<{ modelValue: VarPair[] }>();\nconst emit = defineEmits<{ 'update:modelValue': [VarPair[]]; change: [] }>();\n\nfunction set(i: number, patch: Partial<VarPair>): void {\n const next = props.modelValue.map((e, n) => (n === i ? { ...e, ...patch } : e));\n emit('update:modelValue', next);\n emit('change');\n}\n\nfunction add(): void {\n emit('update:modelValue', [...props.modelValue, { k: '', v: '' }]);\n emit('change');\n}\n\nfunction remove(i: number): void {\n const next = props.modelValue.filter((_, n) => n !== i);\n // Never leave the editor with no rows: an empty list looks like a broken control.\n emit('update:modelValue', next.length ? next : [{ k: '', v: '' }]);\n emit('change');\n}\n</script>\n\n<template>\n <div>\n <div class=\"kv head\"><span>name</span><span>value</span><span /></div>\n <div v-for=\"(e, i) in modelValue\" :key=\"i\" class=\"kv\">\n <input\n type=\"text\"\n :value=\"e.k\"\n :aria-label=\"`variable name ${i + 1}`\"\n placeholder=\"PR\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n @input=\"set(i, { k: ($event.target as HTMLInputElement).value.trim() })\"\n />\n <input\n type=\"text\"\n :value=\"e.v\"\n :aria-label=\"`variable value ${i + 1}`\"\n placeholder=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n @input=\"set(i, { v: ($event.target as HTMLInputElement).value })\"\n />\n <button class=\"sm ghost\" :aria-label=\"`remove variable ${i + 1}`\" @click=\"remove(i)\">−</button>\n </div>\n <button class=\"sm\" @click=\"add\">+ variable</button>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","set","i","patch","next","add","remove","_","n","_createElementBlock","_createElementVNode","_openBlock","_Fragment","_renderList","e","$event","_hoisted_3"],"mappings":"kRAUA,MAAMA,EAAQC,EACRC,EAAOC,EAEb,SAASC,EAAIC,EAAWC,EAA+B,CACrD,MAAMC,EAAOP,EAAM,WAAW,IAAI,CAAC,EAAG,IAAO,IAAMK,EAAI,CAAE,GAAG,EAAG,GAAGC,CAAA,EAAU,CAAE,EAC9EJ,EAAK,oBAAqBK,CAAI,EAC9BL,EAAK,QAAQ,CACf,CAEA,SAASM,GAAY,CACnBN,EAAK,oBAAqB,CAAC,GAAGF,EAAM,WAAY,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEE,EAAK,QAAQ,CACf,CAEA,SAASO,EAAOJ,EAAiB,CAC/B,MAAME,EAAOP,EAAM,WAAW,OAAO,CAACU,EAAGC,IAAMA,IAAMN,CAAC,EAEtDH,EAAK,oBAAqBK,EAAK,OAASA,EAAO,CAAC,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEL,EAAK,QAAQ,CACf,mBAIEU,EAwBM,MAAA,KAAA,aAvBJC,EAAsE,MAAA,CAAjE,MAAM,WAAS,CAACA,EAAiB,YAAX,MAAI,EAAOA,EAAkB,YAAZ,OAAK,EAAOA,EAAQ,MAAA,CAAA,QAChEC,EAAA,EAAA,EAAAF,EAoBMG,EAAA,KAAAC,EApBgBf,EAAA,WAAU,CAAnBgB,EAAGZ,SAAhBO,EAoBM,MAAA,CApB6B,IAAKP,EAAG,MAAM,IAAA,GAC/CQ,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,8BAA6BZ,EAAC,CAAA,GAC/B,YAAY,KACZ,WAAW,QACX,aAAa,MACZ,QAAKa,GAAEd,EAAIC,EAAC,CAAA,EAAQa,EAAO,OAA4B,MAAM,MAAI,CAAA,CAAA,aAEpEL,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,+BAA8BZ,EAAC,CAAA,GAChC,YAAY,IACZ,WAAW,QACX,aAAa,MACZ,QAAKa,GAAEd,EAAIC,KAASa,EAAO,OAA4B,KAAA,CAAK,CAAA,aAE/DL,EAA+F,SAAA,CAAvF,MAAM,WAAY,gCAA+BR,EAAC,CAAA,GAAS,QAAKa,GAAET,EAAOJ,CAAC,CAAA,EAAG,IAAC,EAAAc,CAAA,CAAA,WAExFN,EAAmD,SAAA,CAA3C,MAAM,KAAM,QAAOL,CAAA,EAAK,YAAU,CAAA"}
@@ -0,0 +1 @@
1
+ :root,:root[data-theme=dark]{color-scheme:dark;--bg: #0e1116;--bg-grad: radial-gradient(1100px 620px at 12% -12%, rgba(64, 106, 168, .22) 0%, transparent 70%), radial-gradient(900px 520px at 108% 8%, rgba(96, 76, 168, .13) 0%, transparent 68%);--panel: #171b22;--panel-2: #1e232c;--sunk: #11141a;--line: #2b323d;--line-soft: #21262f;--fg: #eef2f8;--fg-dim: #a4afbe;--fg-mute: #78838f;--accent: #4a9dff;--accent-soft: rgba(74, 157, 255, .18);--ok: #35d17e;--ok-soft: rgba(53, 209, 126, .17);--fail: #ff6159;--fail-soft: rgba(255, 97, 89, .17);--leak: #ffa62b;--leak-soft: rgba(255, 166, 43, .18);--neutral: #95a2b3;--neutral-soft: rgba(149, 162, 179, .16);--hairline: inset 0 1px 0 rgba(255, 255, 255, .055);--hairline-strong: inset 0 1px 0 rgba(255, 255, 255, .09);--elev-1: 0 1px 2px rgba(5, 8, 14, .5);--elev-2: 0 1px 2px rgba(5, 8, 14, .45), 0 6px 16px -4px rgba(5, 8, 14, .5);--elev-3: 0 1px 2px rgba(5, 8, 14, .5), 0 12px 32px -8px rgba(5, 8, 14, .62);--elev-4: 0 2px 4px rgba(5, 8, 14, .5), 0 24px 64px -12px rgba(5, 8, 14, .72);--shadow: var(--elev-2);--hover: rgba(255, 255, 255, .04);--hover-strong: rgba(255, 255, 255, .07);--press: rgba(0, 0, 0, .18);--skel: linear-gradient(90deg, #1c212a 0%, #262d38 50%, #1c212a 100%)}@media(prefers-color-scheme:light){:root:not([data-theme=dark]){color-scheme:light;--bg: #f5f6f8;--bg-grad: radial-gradient(1100px 620px at 12% -12%, rgba(31, 111, 235, .07) 0%, transparent 70%), radial-gradient(900px 520px at 108% 8%, rgba(120, 80, 220, .045) 0%, transparent 68%);--panel: #ffffff;--panel-2: #f4f6f9;--sunk: #eef1f5;--line: #dfe3e9;--line-soft: #eaedf1;--fg: #14181e;--fg-dim: #57616f;--fg-mute: #7d8794;--accent: #1f6feb;--accent-soft: rgba(31, 111, 235, .1);--ok: #1a7f37;--ok-soft: rgba(26, 127, 55, .1);--fail: #cf222e;--fail-soft: rgba(207, 34, 46, .1);--leak: #9a6700;--leak-soft: rgba(154, 103, 0, .12);--neutral: #656d76;--neutral-soft: rgba(101, 109, 118, .1);--hairline: inset 0 1px 0 rgba(255, 255, 255, .9);--hairline-strong: inset 0 1px 0 rgba(255, 255, 255, 1);--elev-1: 0 1px 1px rgba(16, 24, 40, .05);--elev-2: 0 1px 2px rgba(16, 24, 40, .06), 0 4px 12px -2px rgba(16, 24, 40, .07);--elev-3: 0 1px 2px rgba(16, 24, 40, .07), 0 10px 28px -6px rgba(16, 24, 40, .1);--elev-4: 0 2px 4px rgba(16, 24, 40, .07), 0 22px 56px -10px rgba(16, 24, 40, .14);--shadow: var(--elev-2);--hover: rgba(16, 24, 40, .035);--hover-strong: rgba(16, 24, 40, .06);--press: rgba(16, 24, 40, .07);--skel: linear-gradient(90deg, #e9edf2 0%, #f6f8fa 50%, #e9edf2 100%)}}:root[data-theme=light]{color-scheme:light;--bg: #f5f6f8;--bg-grad: radial-gradient(1100px 620px at 12% -12%, rgba(31, 111, 235, .07) 0%, transparent 70%), radial-gradient(900px 520px at 108% 8%, rgba(120, 80, 220, .045) 0%, transparent 68%);--panel: #ffffff;--panel-2: #f4f6f9;--sunk: #eef1f5;--line: #dfe3e9;--line-soft: #eaedf1;--fg: #14181e;--fg-dim: #57616f;--fg-mute: #7d8794;--accent: #1f6feb;--accent-soft: rgba(31, 111, 235, .1);--ok: #1a7f37;--ok-soft: rgba(26, 127, 55, .1);--fail: #cf222e;--fail-soft: rgba(207, 34, 46, .1);--leak: #9a6700;--leak-soft: rgba(154, 103, 0, .12);--neutral: #656d76;--neutral-soft: rgba(101, 109, 118, .1);--hairline: inset 0 1px 0 rgba(255, 255, 255, .9);--hairline-strong: inset 0 1px 0 rgba(255, 255, 255, 1);--elev-1: 0 1px 1px rgba(16, 24, 40, .05);--elev-2: 0 1px 2px rgba(16, 24, 40, .06), 0 4px 12px -2px rgba(16, 24, 40, .07);--elev-3: 0 1px 2px rgba(16, 24, 40, .07), 0 10px 28px -6px rgba(16, 24, 40, .1);--elev-4: 0 2px 4px rgba(16, 24, 40, .07), 0 22px 56px -10px rgba(16, 24, 40, .14);--shadow: var(--elev-2);--hover: rgba(16, 24, 40, .035);--hover-strong: rgba(16, 24, 40, .06);--press: rgba(16, 24, 40, .07);--skel: linear-gradient(90deg, #e9edf2 0%, #f6f8fa 50%, #e9edf2 100%)}:root{--t-xs: 12px;--t-sm: 13.5px;--t-md: 15px;--t-lg: 17px;--t-xl: 22px;--t-2xl: 28px;--t-3xl: 36px;--track-display: -.022em;--track-title: -.014em;--track-body: -.003em;--track-caps: .075em;--lh-tight: 1.15;--lh-body: 1.55;--sans: -apple-system, BlinkMacSystemFont, "Inter var", Inter, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--mono: ui-monospace, SFMono-Regular, "JetBrains Mono", Menlo, Consolas, "Liberation Mono", monospace;--s1: 4px;--s2: 8px;--s3: 12px;--s4: 16px;--s5: 24px;--s6: 32px;--s7: 48px;--r1: 9px;--r2: 13px;--r3: 16px;--r4: 20px;--rail: 216px;--gap-shell: 12px;--measure: 1240px;--ring: 0 0 0 2px var(--bg), 0 0 0 4px color-mix(in srgb, var(--accent) 62%, transparent);--dur-fast: .11s;--dur: .17s;--dur-slow: .28s;--ease: cubic-bezier(.2, .7, .3, 1);--ease-spring: cubic-bezier(.22, 1.4, .36, 1);--stagger-step: 26ms}@media(prefers-reduced-motion:reduce){:root:not([data-motion=full]){--dur-fast: 0ms;--dur: 0ms;--dur-slow: 0ms;--stagger-step: 0ms}:root:not([data-motion=full]) *,:root:not([data-motion=full]) *:before,:root:not([data-motion=full]) *:after{animation-duration:1ms!important;animation-iteration-count:1!important;transition-duration:1ms!important;scroll-behavior:auto!important}:root:not([data-motion=full])::view-transition-old(root),:root:not([data-motion=full])::view-transition-new(root){animation:none!important}}:root[data-motion=none]{--dur-fast: 0ms;--dur: 0ms;--dur-slow: 0ms;--stagger-step: 0ms}:root[data-motion=none] *,:root[data-motion=none] *:before,:root[data-motion=none] *:after{animation-duration:1ms!important;animation-iteration-count:1!important;transition-duration:1ms!important;scroll-behavior:auto!important}:root[data-motion=none]::view-transition-old(root),:root[data-motion=none]::view-transition-new(root){animation:none!important}*,*:before,*:after{box-sizing:border-box}html,body,#app{height:100%}body{margin:0;background:var(--bg);background-image:var(--bg-grad);background-attachment:fixed;color:var(--fg);font:var(--t-md) / var(--lh-body) var(--sans);letter-spacing:var(--track-body);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-variant-numeric:tabular-nums;font-feature-settings:"cv05" 1;overflow-x:hidden}h1,h2,h3,p,ul,figure{margin:0}ul{padding:0;list-style:none}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--mono);font-size:.92em}:focus-visible{outline:none;box-shadow:var(--ring);border-radius:var(--r1)}a:focus-visible{border-radius:4px}.skip{position:absolute;left:-9999px;top:0;z-index:100;padding:var(--s2) var(--s3);background:var(--panel);border:1px solid var(--line);border-radius:var(--r1)}.skip:focus{left:var(--s3);top:var(--s3)}.shell{display:grid;grid-template-columns:var(--rail) minmax(0,1fr);min-height:100%;padding:var(--gap-shell);gap:var(--gap-shell)}.shell.no-rail{grid-template-columns:minmax(0,1fr)}.shell.no-rail .view{max-width:none;min-height:100dvh;display:grid;place-items:center;padding:var(--s5)}.rail{border:1px solid var(--line);border-radius:var(--r4);background:color-mix(in srgb,var(--panel) 82%,transparent);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);padding:var(--s4) var(--s3);position:sticky;top:var(--gap-shell);height:calc(100vh - var(--gap-shell) * 2);display:flex;flex-direction:column;gap:var(--s4);box-shadow:var(--shadow)}.brand{display:flex;align-items:center;gap:var(--s2);font-size:var(--t-lg);font-weight:650;letter-spacing:-.01em;color:var(--fg)}.brand:hover{text-decoration:none}.brand svg{color:var(--accent)}.rail nav{display:flex;flex-direction:column;gap:2px}.navlink{display:flex;align-items:center;gap:var(--s3);padding:var(--s2) var(--s3);border-radius:var(--r1);color:var(--fg-dim);font-weight:500;transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}.navlink:hover{background:var(--panel-2);color:var(--fg);text-decoration:none}.navlink.router-link-active{background:var(--accent-soft);color:var(--accent)}.navlink svg{flex:none}.navlink .count{margin-left:auto;font:600 var(--t-xs) / 1 var(--sans);font-variant-numeric:tabular-nums;color:var(--fg-mute);background:var(--panel-2);padding:3px 6px;border-radius:999px}.rail .foot{margin-top:auto;font-size:var(--t-xs);color:var(--fg-mute);display:grid;gap:var(--s1);word-break:break-all}.rail .foot .foot-who{color:var(--fg-dim);font-weight:550;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.view{min-width:0;padding:var(--s3) var(--s4) var(--s7);max-width:1180px}@media(max-width:800px){.shell{grid-template-columns:minmax(0,1fr);padding:var(--gap-shell) var(--s2) 0}.rail{position:fixed;inset:auto var(--s2) calc(var(--s2) + env(safe-area-inset-bottom)) var(--s2);height:auto;flex-direction:row;align-items:center;gap:var(--s1);padding:var(--s2);background:color-mix(in srgb,var(--panel) 94%,transparent);z-index:20}.rail .brand,.rail .foot{display:none}.rail nav{flex-direction:row;flex:1;justify-content:space-around}.navlink{flex-direction:column;gap:2px;font-size:var(--t-xs);padding:var(--s1) var(--s2);min-width:56px;align-items:center}.navlink .count{display:none}.view{padding:var(--s4) var(--s4) calc(84px + env(safe-area-inset-bottom))}}.page-head{display:flex;align-items:flex-start;gap:var(--s3);flex-wrap:wrap;margin-bottom:var(--s5)}.page-head h1{font-size:var(--t-2xl);font-weight:640;letter-spacing:var(--track-display);line-height:var(--lh-tight)}.page-head .sub{color:var(--fg-dim);font-size:var(--t-sm);margin-top:2px}.grow{flex:1}h2.section{font-size:var(--t-xs);text-transform:uppercase;letter-spacing:var(--track-caps);color:var(--fg-dim);font-weight:620}h3{font-size:var(--t-md);font-weight:600}.dim{color:var(--fg-dim)}.mute{color:var(--fg-mute)}.nowrap{white-space:nowrap}.break{word-break:break-all}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--r3);padding:var(--s4) var(--s4) var(--s5);margin-bottom:var(--s4);min-width:0;container-type:inline-size;box-shadow:var(--hairline),var(--elev-2)}.panel.flush{padding:0;overflow:hidden}.panel>.phead{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap;margin-bottom:var(--s3)}.panel.flush>.phead{padding:var(--s4) var(--s4) 0}.grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--s4);align-items:start}.row{display:flex;gap:var(--s2);align-items:center;flex-wrap:wrap}button,.btn{font:530 var(--t-sm) / 1 var(--sans);letter-spacing:var(--track-body);color:var(--fg);background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r1);padding:var(--s2) var(--s3);min-height:32px;cursor:pointer;display:inline-flex;align-items:center;gap:var(--s2);position:relative;overflow:hidden;box-shadow:var(--hairline),var(--elev-1);transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease),box-shadow var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease)}button:hover:not(:disabled),.btn:hover{border-color:color-mix(in srgb,var(--fg-mute) 60%,var(--line));background:color-mix(in srgb,var(--hover) 100%,var(--panel-2));text-decoration:none}button:active:not(:disabled){transform:translateY(1px);box-shadow:inset 0 2px 4px var(--press)}button:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}button.primary{background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 88%,#fff) 0%,var(--accent) 100%);border-color:color-mix(in srgb,var(--accent) 75%,#000);color:#fff;box-shadow:var(--hairline-strong),var(--elev-2),0 0 color-mix(in srgb,var(--accent) 40%,transparent)}button.primary:hover:not(:disabled){background:linear-gradient(180deg,color-mix(in srgb,var(--accent) 78%,#fff),color-mix(in srgb,var(--accent) 94%,#fff));border-color:color-mix(in srgb,var(--accent) 75%,#000);box-shadow:var(--hairline-strong),var(--elev-2),0 0 18px -2px color-mix(in srgb,var(--accent) 45%,transparent)}button.danger{background:var(--fail-soft);border-color:color-mix(in srgb,var(--fail) 45%,transparent);color:var(--fail)}button.danger:hover:not(:disabled){background:color-mix(in srgb,var(--fail) 26%,transparent);border-color:color-mix(in srgb,var(--fail) 62%,transparent)}button.ghost{background:transparent;box-shadow:none}button.ghost:hover:not(:disabled){background:var(--hover)}button.sm{min-height:27px;padding:var(--s1) var(--s2);font-size:var(--t-xs)}button .progress{position:absolute;inset:auto 0 0 0;height:2px;background:currentColor;opacity:.65;animation:indeterminate 1.1s var(--ease) infinite;transform-origin:left}input[type=text],input[type=password],input[type=search],input[type=url],input[type=email],input[type=number],textarea,select{font:var(--t-sm) / 1.5 var(--sans);letter-spacing:var(--track-body);color:var(--fg);background:var(--sunk);border:1px solid var(--line);border-radius:var(--r1);padding:var(--s2) var(--s3);min-height:32px;width:100%;min-width:0;box-shadow:inset 0 1px 2px var(--press);transition:border-color var(--dur-fast) var(--ease),box-shadow var(--dur-fast) var(--ease)}input[type=text]:hover,input[type=password]:hover,input[type=search]:hover,input[type=url]:hover,input[type=email]:hover,input[type=number]:hover,textarea:hover,select:hover{border-color:color-mix(in srgb,var(--fg-mute) 55%,var(--line))}input[type=text]:focus,input[type=password]:focus,input[type=search]:focus,input[type=url]:focus,input[type=email]:focus,input[type=number]:focus,textarea:focus,select:focus{outline:none;border-color:var(--accent);box-shadow:inset 0 1px 2px var(--press),0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}textarea{font-family:var(--mono);font-size:var(--t-sm);line-height:1.55;resize:vertical;white-space:pre;overflow-wrap:normal;overflow-x:auto}input::placeholder,textarea::placeholder{color:var(--fg-mute)}input:-webkit-autofill,input:-webkit-autofill:hover,input:-webkit-autofill:focus{-webkit-box-shadow:0 0 0 1000px var(--sunk) inset;-webkit-text-fill-color:var(--fg);caret-color:var(--fg);transition:background-color 9999s ease-out}label{font-size:var(--t-sm);color:var(--fg-dim)}.field{display:grid;gap:var(--s2);min-width:0}.field+.field,.field+.hint,.hint+.field{margin-top:var(--s4)}.row>.field+.field,.row>.field+.hint,.row>.hint+.field{margin-top:0}.row.wrap>.field,.row>.field{align-self:stretch}.check{display:inline-flex;align-items:center;gap:var(--s2);cursor:pointer;color:var(--fg)}.check input{width:auto;min-height:0;accent-color:var(--accent)}.kv{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto;gap:var(--s2);margin-bottom:var(--s2);align-items:center;max-width:640px}.kv.head{font-size:var(--t-xs);color:var(--fg-mute);text-transform:uppercase;letter-spacing:.06em}@media(max-width:560px){.kv{grid-template-columns:minmax(0,1fr) auto}.kv.head{display:none}}.badge{display:inline-flex;align-items:center;gap:5px;font:500 var(--t-xs) / 1 var(--sans);padding:4px 8px;border-radius:999px;background:var(--neutral-soft);color:var(--fg-dim);border:1px solid transparent;white-space:nowrap}.badge.ok{background:var(--ok-soft);color:var(--ok)}.badge.failed,.badge.fail{background:var(--fail-soft);color:var(--fail)}.badge.leaked,.badge.warn{background:var(--leak-soft);color:var(--leak)}.badge.running,.badge.busy,.badge.info{background:var(--accent-soft);color:var(--accent)}.badge.unknown{background:var(--neutral-soft);color:var(--neutral)}.badge.shared{background:var(--leak-soft);color:var(--leak)}.badge.isolated{background:var(--accent-soft);color:var(--accent)}.badge.off{opacity:.38}.badge .dot{width:6px;height:6px;border-radius:50%;background:currentColor}.badge.running .dot,.dot.pulse{animation:pulse 1.6s var(--ease) infinite}.banner{border:1px solid var(--line);border-left-width:3px;border-radius:var(--r2);padding:var(--s3) var(--s4);margin:var(--s3) 0;background:var(--panel-2);font-size:var(--t-sm)}.banner b{color:var(--fg)}.banner p{margin-top:var(--s2);color:var(--fg-dim)}.banner.ok{border-left-color:var(--ok);background:var(--ok-soft)}.banner.failed{border-left-color:var(--fail);background:var(--fail-soft)}.banner.leaked,.banner.warn{border-left-color:var(--leak);background:var(--leak-soft)}.banner.info{border-left-color:var(--accent);background:var(--accent-soft)}.banner.plain{border-left-color:var(--fg-mute)}.hint{font-size:var(--t-sm);color:var(--fg-mute);margin-top:var(--s3)}.hint code,.banner code{background:var(--sunk);padding:1px 5px;border-radius:4px}table{width:100%;border-collapse:collapse;font-size:var(--t-sm)}th{text-align:left;font:620 var(--t-xs) / 1 var(--sans);text-transform:uppercase;letter-spacing:var(--track-caps);color:var(--fg-mute);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:1;background:var(--panel);white-space:nowrap}td{padding:var(--s2) var(--s3);border-bottom:1px solid var(--line-soft);vertical-align:middle}tbody tr{transition:background var(--dur-fast) var(--ease)}tbody tr:hover{background:var(--hover)}tbody tr:last-child td{border-bottom:none}td.name{font-weight:600;word-break:break-all}@media(max-width:640px){table.cards,table.cards tbody,table.cards tr,table.cards td{display:block;width:100%}table.cards thead{display:none}table.cards tr{border:1px solid var(--line);border-radius:var(--r2);padding:var(--s2) var(--s3);margin-bottom:var(--s2);background:var(--panel-2)}table.cards td{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s3);border:0;padding:var(--s1) 0;text-align:right}table.cards td:before{content:attr(data-label);font:600 var(--t-xs) / 1.6 var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--fg-mute);text-align:left;flex:none}table.cards td[colspan]{text-align:left}table.cards td[colspan]:before{content:""}}.kvlist{display:grid;gap:1px;background:var(--line-soft);border-radius:var(--r2);overflow:hidden}.kvlist>li{display:flex;gap:var(--s3);padding:var(--s3);background:var(--panel);align-items:baseline;flex-wrap:wrap}.kvlist>li>.k{flex:none;width:128px;color:var(--fg-dim);font-size:var(--t-sm)}.kvlist>li>.v{flex:1;min-width:0;word-break:break-word}@media(max-width:480px){.kvlist>li>.k{width:100%}}pre.raw,pre.log{font-family:var(--mono);font-size:var(--t-sm);line-height:1.6;background:var(--sunk);border:1px solid var(--line);border-radius:var(--r2);padding:var(--s3);margin:var(--s3) 0 0;overflow:auto;white-space:pre}pre.raw{color:var(--fail);max-height:420px}pre.code{font-family:var(--mono);font-size:var(--t-sm);line-height:1.6;background:var(--sunk);border:1px solid var(--line);border-radius:var(--r2);padding:var(--s3);margin:0;max-height:520px;overflow:auto;white-space:pre;color:var(--fg-dim)}pre.log{max-height:460px;color:var(--fg-dim)}pre.log .t{color:var(--fg-mute)}pre.log .lv-error{color:var(--fail)}pre.log .lv-warn{color:var(--leak)}pre.log .lv-info{color:var(--fg)}pre.log mark{background:var(--leak-soft);color:var(--leak)}.masked{color:var(--fg-mute);letter-spacing:.12em}.stats{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--s2)}@container (min-width: 680px){.stats{grid-template-columns:repeat(6,1fr)}}.stat{background:var(--panel-2);border:1px solid var(--line-soft);border-radius:var(--r2);padding:var(--s3);box-shadow:var(--hairline);min-width:0}.stat .v{font:600 var(--t-2xl) / 1.1 var(--sans);letter-spacing:var(--track-display);font-variant-numeric:tabular-nums}.stat[data-zero=true] .v{color:var(--fg-mute);font-weight:500}.stat .k{font-size:var(--t-xs);color:var(--fg-mute);text-transform:uppercase;letter-spacing:.04em;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stat.unknown .v{color:var(--neutral)}.stat.busy .v{color:var(--accent)}.stat.leak .v{color:var(--leak)}.tabs{display:flex;gap:var(--s1);border-bottom:1px solid var(--line);margin-bottom:var(--s4);overflow-x:auto;scrollbar-width:none}.tabs::-webkit-scrollbar{display:none}.tabs a{padding:var(--s2) var(--s3);color:var(--fg-dim);font-size:var(--t-sm);font-weight:500;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;transition:color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}.tabs a:hover{color:var(--fg);text-decoration:none}.tabs a.router-link-active{color:var(--fg);border-bottom-color:var(--accent)}.steps{display:grid;gap:1px;background:var(--line-soft);border-radius:var(--r2);overflow:hidden}.step{display:grid;grid-template-columns:24px minmax(0,1fr);gap:var(--s3);padding:var(--s3);background:var(--panel);align-items:start}.step .mark{font:600 var(--t-lg) / 1.2 var(--sans);text-align:center}.step .who{font-weight:550;word-break:break-all}.step .msg{font-size:var(--t-sm);color:var(--fg-dim);word-break:break-word}.s-ok{color:var(--ok)}.s-failed{color:var(--fail)}.s-leaked{color:var(--leak)}.s-unverifiable{color:var(--neutral)}.skel{background:var(--skel);background-size:200% 100%;border-radius:var(--r1);height:14px;animation:shimmer 1.3s linear infinite}.skel+.skel{margin-top:var(--s2)}.skel.tall{height:44px}.skel.w60{width:60%}.skel.w40{width:40%}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.8)}}@keyframes indeterminate{0%{transform:scaleX(0)}50%{transform:scaleX(.7)}to{transform:scaleX(0);transform-origin:right}}@keyframes rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@keyframes land{0%{transform:scale(.985);opacity:.4}60%{transform:scale(1.004)}to{transform:none;opacity:1}}.stagger>*{animation:rise var(--dur) var(--ease) backwards;animation-delay:calc(var(--i, 0) * var(--stagger-step))}.land{animation:land var(--dur-slow) var(--ease)}.view-enter-active,.view-leave-active{transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease)}.view-enter-from{opacity:0;transform:translateY(6px)}.view-leave-to{opacity:0;transform:translateY(-4px)}.toasts{position:fixed;right:var(--s4);bottom:var(--s4);z-index:50;display:grid;gap:var(--s2);width:min(360px,calc(100vw - var(--s5)))}@media(max-width:800px){.toasts{bottom:calc(84px + env(safe-area-inset-bottom))}}.toast{display:flex;gap:var(--s3);align-items:flex-start;background:var(--panel);border:1px solid var(--line);border-radius:var(--r2);padding:var(--s3) var(--s3) var(--s3) var(--s2);box-shadow:var(--hairline),var(--elev-4);font-size:var(--t-sm);position:relative}.toast:before{content:"";flex:none;width:8px;height:8px;margin:6px 0 0 var(--s2);border-radius:50%;background:var(--neutral);box-shadow:0 0 0 3px var(--neutral-soft)}.toast.ok:before{background:var(--ok);box-shadow:0 0 0 3px var(--ok-soft)}.toast.error:before{background:var(--fail);box-shadow:0 0 0 3px var(--fail-soft)}.toast.warn:before{background:var(--leak);box-shadow:0 0 0 3px var(--leak-soft)}.toast.info:before{background:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.toast .x{margin-left:auto;background:none;border:0;min-height:0;padding:2px 6px;color:var(--fg-mute)}.toast-enter-active,.toast-leave-active{transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease)}.toast-enter-from,.toast-leave-to{opacity:0;transform:translate(12px)}.toast-move{transition:transform var(--dur) var(--ease)}.scrim{position:fixed;top:0;right:0;bottom:0;left:0;background:#0000008c;z-index:60;display:grid;place-items:center;padding:var(--s4)}.sheet{background:var(--panel);border:1px solid var(--line);border-radius:var(--r3);box-shadow:var(--shadow);padding:var(--s5);width:min(460px,100%);max-height:80vh;overflow:auto}.sheet dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--s2) var(--s4);margin:var(--s4) 0 0;align-items:baseline}.sheet dt{text-align:right}.sheet dd{margin:0;color:var(--fg-dim);font-size:var(--t-sm)}kbd{font:500 var(--t-xs) / 1 var(--mono);background:var(--sunk);border:1px solid var(--line);border-bottom-width:2px;border-radius:5px;padding:4px 6px;color:var(--fg)}.fade-enter-active,.fade-leave-active{transition:opacity var(--dur) var(--ease)}.fade-enter-from,.fade-leave-to{opacity:0}.hint-wrap{position:relative;display:inline-flex;vertical-align:middle;margin-inline-start:var(--s1)}.hint-btn{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;min-height:0;padding:0;border:0;border-radius:999px;background:none;color:var(--fg-mute);cursor:pointer;transition:color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}.hint-btn:hover,.hint-btn.on{color:var(--accent);background:var(--accent-soft)}.hint-bub{position:absolute;z-index:40;width:max-content;max-width:min(300px,74vw);padding:var(--s2) var(--s3);background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r2);box-shadow:var(--shadow);color:var(--fg-dim);font:400 var(--t-sm) / 1.5 var(--sans);text-transform:none;letter-spacing:normal;white-space:normal;text-align:start;pointer-events:none}.hint-bub.below{top:calc(100% + 6px)}.hint-bub.above{bottom:calc(100% + 6px)}.hint-bub.to-start{inset-inline-start:-2px}.hint-bub.to-end{inset-inline-end:-2px}.hint-bub code{color:var(--fg)}.hint-enter-active,.hint-leave-active{transition:opacity var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease)}.hint-enter-from,.hint-leave-to{opacity:0}.hint-bub.below.hint-enter-from,.hint-bub.below.hint-leave-to{transform:translateY(-3px)}.hint-bub.above.hint-enter-from,.hint-bub.above.hint-leave-to{transform:translateY(3px)}.palette-scrim{align-items:flex-start;padding-top:12vh}.palette{width:min(620px,calc(100vw - var(--s5)));background:var(--panel);border:1px solid var(--line);border-radius:var(--r3);box-shadow:var(--hairline-strong),var(--elev-4);overflow:hidden;display:flex;flex-direction:column;max-height:min(60vh,560px)}.palette-q{display:flex;align-items:center;gap:var(--s3);padding:var(--s3) var(--s4);border-bottom:1px solid var(--line-soft);color:var(--fg-mute)}.palette-q input{flex:1;background:none;border:none;box-shadow:none;padding:0;min-height:26px;font-size:var(--t-lg);letter-spacing:var(--track-title);color:var(--fg)}.palette-q input:focus{outline:none;border:none;box-shadow:none}.palette-list{overflow-y:auto;padding:var(--s2);min-height:0}.palette-group{font-size:var(--t-xs);text-transform:uppercase;letter-spacing:var(--track-caps);color:var(--fg-mute);padding:var(--s3) var(--s2) var(--s1)}.palette-row{display:flex;align-items:center;gap:var(--s3);width:100%;text-align:left;background:none;border:none;box-shadow:none;border-radius:var(--r1);padding:var(--s2) var(--s3);min-height:34px;color:var(--fg)}.palette-row[data-on=true]{background:var(--accent-soft);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--accent) 32%,transparent)}.palette-label{font-weight:520}.palette-hint{margin-left:auto;color:var(--fg-mute);font-size:var(--t-xs);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}.palette-empty{color:var(--fg-mute);padding:var(--s5) var(--s3);text-align:center;font-size:var(--t-sm)}.palette-foot{display:flex;gap:var(--s4);align-items:center;padding:var(--s2) var(--s4);border-top:1px solid var(--line-soft);background:var(--panel-2);color:var(--fg-mute);font-size:var(--t-xs)}kbd{font:500 11px / 1 var(--mono);color:var(--fg-dim);background:var(--sunk);border:1px solid var(--line);border-radius:5px;padding:3px 5px;margin-right:3px}.fade-enter-active .palette{animation:palette-in var(--dur-slow) var(--ease-spring)}@keyframes palette-in{0%{opacity:0;transform:translateY(-10px) scale(.98)}to{opacity:1;transform:none}}::view-transition-old(root),::view-transition-new(root){animation-duration:var(--dur);animation-timing-function:var(--ease)}::view-transition-old(root){animation-name:vt-out}::view-transition-new(root){animation-name:vt-in}@keyframes vt-out{to{opacity:0}}@keyframes vt-in{0%{opacity:0;transform:translateY(4px)}}.searchbox{display:flex;align-items:center;gap:var(--s2);flex:1;min-width:180px;padding-inline:var(--s3);background:var(--sunk);border:1px solid var(--line);border-radius:var(--r1);box-shadow:inset 0 1px 2px var(--press);color:var(--fg-mute);min-height:34px;transition:border-color var(--dur-fast) var(--ease),box-shadow var(--dur-fast) var(--ease)}.searchbox:focus-within{border-color:var(--accent);box-shadow:inset 0 1px 2px var(--press),0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}.searchbox input{border:none;background:none;box-shadow:none;padding:0;min-height:0}.searchbox input:focus{outline:none;border:none;box-shadow:none}.searchbox input::-webkit-search-cancel-button{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.compact{width:auto;min-height:34px;padding-inline-end:var(--s5)}.phead>.check{min-height:34px}.field>.selm-trigger,.selm-trigger{max-width:340px;width:100%}.selm-trigger{display:inline-flex;align-items:center;gap:var(--s2);justify-content:space-between;min-height:34px;padding:0 var(--s2) 0 var(--s3);background:var(--sunk);border:1px solid var(--line);border-radius:var(--r1);box-shadow:inset 0 1px 2px var(--press);font:var(--t-sm) / 1 var(--sans);color:var(--fg);cursor:pointer}.selm-trigger:hover:not(:disabled){border-color:color-mix(in srgb,var(--fg-mute) 55%,var(--line))}.selm-trigger[data-state=open]{border-color:var(--accent);box-shadow:inset 0 1px 2px var(--press),0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}.selm-value{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.selm-caret{flex:none;color:var(--fg-mute);transition:transform var(--dur-fast) var(--ease)}.selm-trigger[data-state=open] .selm-caret{transform:rotate(180deg)}.selm-list{z-index:61;padding:var(--s1);max-height:280px;overflow-y:auto;background:var(--panel);border:1px solid var(--line);border-radius:var(--r2);box-shadow:var(--hairline-strong),var(--elev-4);min-width:var(--reka-select-trigger-width);animation:palette-in var(--dur) var(--ease-spring)}.selm-opt{display:flex;align-items:center;gap:var(--s2);padding:var(--s2);min-height:30px;border-radius:calc(var(--r1) - 2px);font-size:var(--t-sm);cursor:pointer;white-space:nowrap;-webkit-user-select:none;user-select:none}.selm-opt[data-highlighted]{background:var(--accent-soft);outline:none}.selm-opt[data-disabled]{opacity:.45;cursor:not-allowed}.selm-hint{color:var(--fg-mute);font-size:var(--t-xs);margin-inline-start:auto}.selm-tick{margin-inline-start:auto;color:var(--accent);display:inline-flex}.selm-hint+.selm-tick{margin-inline-start:var(--s2)}input[type=checkbox],input[type=radio]{-moz-appearance:none;appearance:none;-webkit-appearance:none;flex:none;width:17px;height:17px;margin:0;border:1px solid color-mix(in srgb,var(--fg-mute) 60%,var(--line));background:var(--sunk);box-shadow:inset 0 1px 2px var(--press);cursor:pointer;display:grid;place-content:center;transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}input[type=checkbox]{border-radius:5px}input[type=radio]{border-radius:50%}input[type=checkbox]:hover:not(:disabled),input[type=radio]:hover:not(:disabled){border-color:var(--accent)}input[type=checkbox]:before,input[type=radio]:before{content:"";transform:scale(0);transition:transform var(--dur-fast) var(--ease-spring)}input[type=checkbox]:before{width:11px;height:11px;background:#fff;clip-path:polygon(14% 47%,0 61%,40% 100%,100% 22%,85% 9%,39% 71%)}input[type=radio]:before{width:7px;height:7px;border-radius:50%;background:#fff}input[type=checkbox]:checked,input[type=radio]:checked{background:var(--accent);border-color:var(--accent)}input[type=checkbox]:checked:before,input[type=radio]:checked:before{transform:scale(1)}input[type=checkbox]:disabled,input[type=radio]:disabled{opacity:.45;cursor:not-allowed}.helpq{display:inline-grid;place-items:center;width:18px;height:18px;padding:0;min-height:0;border-radius:50%;border:1px solid var(--line);background:var(--panel-2);box-shadow:none;color:var(--fg-mute);cursor:pointer;vertical-align:middle}.helpq:hover{color:var(--accent);border-color:var(--accent);background:var(--accent-soft)}.help-scrim{-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);z-index:70}.help-modal{position:fixed;z-index:71;top:50%;left:50%;transform:translate(-50%,-50%);width:min(620px,calc(100vw - var(--s5)));max-height:min(80vh,720px);display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);border-radius:var(--r3);box-shadow:var(--hairline-strong),var(--elev-4);animation:help-in var(--dur-slow) var(--ease-spring)}@keyframes help-in{0%{opacity:0;transform:translate(-50%,-46%) scale(.97)}}.help-head{display:flex;align-items:center;gap:var(--s3);padding:var(--s4) var(--s4) var(--s3);border-bottom:1px solid var(--line-soft)}.help-title{font-size:var(--t-lg);font-weight:620;letter-spacing:var(--track-title);margin:0;flex:1}.help-body{padding:var(--s4);overflow-y:auto;color:var(--fg-dim);font-size:var(--t-sm);line-height:var(--lh-body)}.help-body>*+*{margin-top:var(--s3)}.help-body b,.help-body strong{color:var(--fg)}.check-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:var(--s2) var(--s4);margin-top:var(--s2)}.check-grid .check{min-height:26px}.settings-form .field input,.settings-form input[type=url],.settings-form input[type=password],.settings-form input[type=text]{max-width:560px}.settings-form .hint,.settings-form .mute.hint{max-width:640px}.lv{border:1px solid var(--line);border-radius:var(--r2);background:var(--sunk);overflow:hidden}.lv-bar{display:flex;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line-soft);font-size:var(--t-xs);color:var(--fg-mute)}.lv-box{max-height:520px;overflow-y:auto;padding:var(--s2) 0 var(--s3);font:var(--t-sm) / 1.6 var(--mono);overscroll-behavior:contain}.lv-empty{color:var(--fg-mute);padding:var(--s4) var(--s3);font-family:var(--sans)}.lv-row{display:grid;grid-template-columns:auto minmax(0,1fr);gap:0 var(--s3);padding:1px var(--s3);content-visibility:auto;contain-intrinsic-block-size:auto 22px}.lv-row:hover{background:var(--hover)}.lv-gutter{white-space:nowrap;color:var(--fg-mute);-webkit-user-select:none;user-select:none;font-variant-numeric:tabular-nums}.lv-msg{white-space:pre-wrap;overflow-wrap:anywhere;color:var(--fg-dim)}.lv-row.error .lv-msg{color:var(--fail)}.lv-row.warn .lv-msg{color:var(--leak)}.lv-row.ok .lv-msg{color:var(--ok)}.lv-row.svc-0 .lv-gutter{color:#7aa2f7}.lv-row.svc-1 .lv-gutter{color:#9ece6a}.lv-row.svc-2 .lv-gutter{color:#e0af68}.lv-row.svc-3 .lv-gutter{color:#bb9af7}.lv-row.svc-4 .lv-gutter{color:#7dcfff}.lv-row.svc-5 .lv-gutter{color:#f7768e}.lv-sep{display:flex;align-items:center;gap:var(--s3);padding:var(--s2) var(--s3);color:var(--fg-mute);font-family:var(--sans);font-size:var(--t-xs)}.lv-sep:before,.lv-sep:after{content:"";flex:1;border-top:1px solid var(--line-soft)}