@samyx/preview-stacks-ui 0.38.1 → 0.39.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/ActionButton-OewKsFrK.js +2 -0
- package/dist/assets/ActionButton-OewKsFrK.js.map +1 -0
- package/dist/assets/ActionButton-jjW2X4R6.css +1 -0
- package/dist/assets/AxesTab-DSzgRhgG.js +2 -0
- package/dist/assets/AxesTab-DSzgRhgG.js.map +1 -0
- package/dist/assets/ConfigTab-DhtUF0Dh.js +2 -0
- package/dist/assets/ConfigTab-DhtUF0Dh.js.map +1 -0
- package/dist/assets/ConfigView-wXO4j8H7.js +2 -0
- package/dist/assets/ConfigView-wXO4j8H7.js.map +1 -0
- package/dist/assets/ConflictNote.vue_vue_type_script_setup_true_lang-B2fgJAmK.js +2 -0
- package/dist/assets/ConflictNote.vue_vue_type_script_setup_true_lang-B2fgJAmK.js.map +1 -0
- package/dist/assets/ControlView-ninZ6BIV.js +2 -0
- package/dist/assets/ControlView-ninZ6BIV.js.map +1 -0
- package/dist/assets/DangerTab-D2dFrj4t.js +2 -0
- package/dist/assets/DangerTab-D2dFrj4t.js.map +1 -0
- package/dist/assets/DashboardView-FpEFhaCl.js +2 -0
- package/dist/assets/DashboardView-FpEFhaCl.js.map +1 -0
- package/dist/assets/DeploymentDetailView-B1dKUzvp.js +2 -0
- package/dist/assets/DeploymentDetailView-B1dKUzvp.js.map +1 -0
- package/dist/assets/DeploymentsView-_V93UC6F.js +2 -0
- package/dist/assets/DeploymentsView-_V93UC6F.js.map +1 -0
- package/dist/assets/EquivalentCommand-D-mkbReY.js +3 -0
- package/dist/assets/EquivalentCommand-D-mkbReY.js.map +1 -0
- package/dist/assets/{EquivalentCommand-BFnZRfDg.css → EquivalentCommand-DWqoDHJi.css} +1 -1
- package/dist/assets/ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js +2 -0
- package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-Imao12aK.js.map → ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js.map} +1 -1
- package/dist/assets/JobDetailView-RD5MxdA7.js +2 -0
- package/dist/assets/JobDetailView-RD5MxdA7.js.map +1 -0
- package/dist/assets/JobsView-BJrH9ViP.js +2 -0
- package/dist/assets/JobsView-BJrH9ViP.js.map +1 -0
- package/dist/assets/JobsView-BhdjSbKx.css +1 -0
- package/dist/assets/LogViewer.vue_vue_type_script_setup_true_lang-BRSt9Exf.js +3 -0
- package/dist/assets/LogViewer.vue_vue_type_script_setup_true_lang-BRSt9Exf.js.map +1 -0
- package/dist/assets/LoginView-wAfA0Ozk.css +1 -0
- package/dist/assets/LoginView-zWd9FvKS.js +2 -0
- package/dist/assets/LoginView-zWd9FvKS.js.map +1 -0
- package/dist/assets/LogsTab--Q-KHeWj.js +6 -0
- package/dist/assets/LogsTab--Q-KHeWj.js.map +1 -0
- package/dist/assets/NotFoundView-1lI66Xt-.js +2 -0
- package/dist/assets/NotFoundView-1lI66Xt-.js.map +1 -0
- package/dist/assets/NotifiersView-BfWfRzg4.js +2 -0
- package/dist/assets/NotifiersView-BfWfRzg4.js.map +1 -0
- package/dist/assets/OverviewTab-P3htOnPs.js +2 -0
- package/dist/assets/OverviewTab-P3htOnPs.js.map +1 -0
- package/dist/assets/PublicLogsView-7OGRBbDd.css +1 -0
- package/dist/assets/PublicLogsView-DcDQMivg.js +4 -0
- package/dist/assets/PublicLogsView-DcDQMivg.js.map +1 -0
- package/dist/assets/RefreshButton.vue_vue_type_script_setup_true_lang-CQvD4Out.js +2 -0
- package/dist/assets/{RefreshButton.vue_vue_type_script_setup_true_lang-C5aXqJdu.js.map → RefreshButton.vue_vue_type_script_setup_true_lang-CQvD4Out.js.map} +1 -1
- package/dist/assets/RegistriesView-9hPjVsOW.js +2 -0
- package/dist/assets/RegistriesView-9hPjVsOW.js.map +1 -0
- package/dist/assets/RelativeTime.vue_vue_type_script_setup_true_lang-BxIqRu_b.js +2 -0
- package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-ze6IGxQV.js.map → RelativeTime.vue_vue_type_script_setup_true_lang-BxIqRu_b.js.map} +1 -1
- package/dist/assets/RequiresTab--C-3QPQB.js +2 -0
- package/dist/assets/RequiresTab--C-3QPQB.js.map +1 -0
- package/dist/assets/{RouteTarget.vue_vue_type_script_setup_true_lang-B1xfCOxN.js → RouteTarget.vue_vue_type_script_setup_true_lang-D3FLq6Mt.js} +2 -2
- package/dist/assets/{RouteTarget.vue_vue_type_script_setup_true_lang-B1xfCOxN.js.map → RouteTarget.vue_vue_type_script_setup_true_lang-D3FLq6Mt.js.map} +1 -1
- package/dist/assets/RoutingView-CC8xPrcT.js +9 -0
- package/dist/assets/RoutingView-CC8xPrcT.js.map +1 -0
- package/dist/assets/RuntimeTab-Do1xfTp8.js +2 -0
- package/dist/assets/RuntimeTab-Do1xfTp8.js.map +1 -0
- package/dist/assets/{RuntimeTab-Y2qBJykM.css → RuntimeTab-PJh7Vu6S.css} +1 -1
- package/dist/assets/SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js +2 -0
- package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-JY8m6tfk.js.map → SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js.map} +1 -1
- package/dist/assets/SettingsView-Dxbl6HZA.js +2 -0
- package/dist/assets/SettingsView-Dxbl6HZA.js.map +1 -0
- package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-DMvuIcZF.js → SkeletonList.vue_vue_type_script_setup_true_lang-D4pkKZAr.js} +2 -2
- package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-DMvuIcZF.js.map → SkeletonList.vue_vue_type_script_setup_true_lang-D4pkKZAr.js.map} +1 -1
- package/dist/assets/SpecDetailView-DyFAAMfr.js +2 -0
- package/dist/assets/SpecDetailView-DyFAAMfr.js.map +1 -0
- package/dist/assets/SpecsView-CfV1_GDx.js +2 -0
- package/dist/assets/SpecsView-CfV1_GDx.js.map +1 -0
- package/dist/assets/{SsoMark.vue_vue_type_script_setup_true_lang-BXhQGdjJ.js → SsoMark.vue_vue_type_script_setup_true_lang-BU6ThRhq.js} +2 -2
- package/dist/assets/{SsoMark.vue_vue_type_script_setup_true_lang-BXhQGdjJ.js.map → SsoMark.vue_vue_type_script_setup_true_lang-BU6ThRhq.js.map} +1 -1
- package/dist/assets/SsoView-BlYFW0pX.css +1 -0
- package/dist/assets/SsoView-CxFylJ9d.js +2 -0
- package/dist/assets/SsoView-CxFylJ9d.js.map +1 -0
- package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-jkd_ejVw.js → StateBadge.vue_vue_type_script_setup_true_lang-CfjzlWSc.js} +2 -2
- package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-jkd_ejVw.js.map → StateBadge.vue_vue_type_script_setup_true_lang-CfjzlWSc.js.map} +1 -1
- package/dist/assets/SubmitView-BcAcTlwC.js +50 -0
- package/dist/assets/SubmitView-BcAcTlwC.js.map +1 -0
- package/dist/assets/SwarmView-BHtmW4QF.js +2 -0
- package/dist/assets/SwarmView-BHtmW4QF.js.map +1 -0
- package/dist/assets/{TerminalTab-DbjRludT.css → TerminalTab-BtiSvTLn.css} +1 -1
- package/dist/assets/TerminalTab-DF8cSjcg.js +3 -0
- package/dist/assets/TerminalTab-DF8cSjcg.js.map +1 -0
- package/dist/assets/UsersView-BBoM6joP.css +1 -0
- package/dist/assets/UsersView-CbPtmnP6.js +2 -0
- package/dist/assets/UsersView-CbPtmnP6.js.map +1 -0
- package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-uS04X_Kj.js → VarEditor.vue_vue_type_script_setup_true_lang-BNQTEru_.js} +2 -2
- package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-uS04X_Kj.js.map → VarEditor.vue_vue_type_script_setup_true_lang-BNQTEru_.js.map} +1 -1
- package/dist/assets/{VarIO-DkbGdmrU.js → VarIO-BFBaI8oU.js} +6 -6
- package/dist/assets/{VarIO-DkbGdmrU.js.map → VarIO-BFBaI8oU.js.map} +1 -1
- package/dist/assets/VariablesView-B2YuiIkD.css +1 -0
- package/dist/assets/VariablesView-BEcI8Bi0.js +2 -0
- package/dist/assets/VariablesView-BEcI8Bi0.js.map +1 -0
- package/dist/assets/index-D4djwlnX.css +1 -0
- package/dist/assets/index-D_Ks6aQi.js +166 -0
- package/dist/assets/index-D_Ks6aQi.js.map +1 -0
- package/dist/assets/useDeployment-DFuBcVnK.js +2 -0
- package/dist/assets/{useDeployment-D3f7oVQg.js.map → useDeployment-DFuBcVnK.js.map} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/assets/ActionButton.vue_vue_type_script_setup_true_lang-CB94t3xm.js +0 -2
- package/dist/assets/ActionButton.vue_vue_type_script_setup_true_lang-CB94t3xm.js.map +0 -1
- package/dist/assets/AxesTab-C0OeAG-v.js +0 -2
- package/dist/assets/AxesTab-C0OeAG-v.js.map +0 -1
- package/dist/assets/ConfigTab-CqQK7vQl.js +0 -2
- package/dist/assets/ConfigTab-CqQK7vQl.js.map +0 -1
- package/dist/assets/ConfigView-D6Ourc5o.js +0 -2
- package/dist/assets/ConfigView-D6Ourc5o.js.map +0 -1
- package/dist/assets/ConflictNote.vue_vue_type_script_setup_true_lang-BtSm_ZhU.js +0 -2
- package/dist/assets/ConflictNote.vue_vue_type_script_setup_true_lang-BtSm_ZhU.js.map +0 -1
- package/dist/assets/ControlView-G6A_-Og_.js +0 -2
- package/dist/assets/ControlView-G6A_-Og_.js.map +0 -1
- package/dist/assets/DangerTab-Cn06aqx9.js +0 -2
- package/dist/assets/DangerTab-Cn06aqx9.js.map +0 -1
- package/dist/assets/DashboardView-CpJmNcFL.js +0 -2
- package/dist/assets/DashboardView-CpJmNcFL.js.map +0 -1
- package/dist/assets/DeploymentDetailView-CP2ANsF3.js +0 -2
- package/dist/assets/DeploymentDetailView-CP2ANsF3.js.map +0 -1
- package/dist/assets/DeploymentsView-CXQxjAem.js +0 -2
- package/dist/assets/DeploymentsView-CXQxjAem.js.map +0 -1
- package/dist/assets/DialogTrigger-BlkbgxBa.js +0 -2
- package/dist/assets/DialogTrigger-BlkbgxBa.js.map +0 -1
- package/dist/assets/EquivalentCommand-CbJfYQaC.js +0 -3
- package/dist/assets/EquivalentCommand-CbJfYQaC.js.map +0 -1
- package/dist/assets/ErrorNote.vue_vue_type_script_setup_true_lang-Imao12aK.js +0 -2
- package/dist/assets/HelpModal.vue_vue_type_script_setup_true_lang-CjoiZrMf.js +0 -2
- package/dist/assets/HelpModal.vue_vue_type_script_setup_true_lang-CjoiZrMf.js.map +0 -1
- package/dist/assets/JobDetailView-lYBZcOSg.js +0 -2
- package/dist/assets/JobDetailView-lYBZcOSg.js.map +0 -1
- package/dist/assets/JobsView-CC7lre3Q.css +0 -1
- package/dist/assets/JobsView-CyZXyEfc.js +0 -2
- package/dist/assets/JobsView-CyZXyEfc.js.map +0 -1
- package/dist/assets/LogViewer.vue_vue_type_script_setup_true_lang-CBVTTiPn.js +0 -3
- package/dist/assets/LogViewer.vue_vue_type_script_setup_true_lang-CBVTTiPn.js.map +0 -1
- package/dist/assets/LoginView-C90w1dZ2.css +0 -1
- package/dist/assets/LoginView-Do6LVtZ6.js +0 -2
- package/dist/assets/LoginView-Do6LVtZ6.js.map +0 -1
- package/dist/assets/LogsTab-DMj2p8j8.js +0 -6
- package/dist/assets/LogsTab-DMj2p8j8.js.map +0 -1
- package/dist/assets/NotFoundView-DyiW8m6P.js +0 -2
- package/dist/assets/NotFoundView-DyiW8m6P.js.map +0 -1
- package/dist/assets/NotifiersView-CIWU3Ohd.js +0 -2
- package/dist/assets/NotifiersView-CIWU3Ohd.js.map +0 -1
- package/dist/assets/OverviewTab-tJGh_KZG.js +0 -2
- package/dist/assets/OverviewTab-tJGh_KZG.js.map +0 -1
- package/dist/assets/PublicLogsView-BAe90EbI.css +0 -1
- package/dist/assets/PublicLogsView-CwLsI-Nz.js +0 -4
- package/dist/assets/PublicLogsView-CwLsI-Nz.js.map +0 -1
- package/dist/assets/RefreshButton.vue_vue_type_script_setup_true_lang-C5aXqJdu.js +0 -2
- package/dist/assets/RegistriesView-399hTPKi.js +0 -2
- package/dist/assets/RegistriesView-399hTPKi.js.map +0 -1
- package/dist/assets/RelativeTime.vue_vue_type_script_setup_true_lang-ze6IGxQV.js +0 -2
- package/dist/assets/RequiresTab-B53DZ6Dn.js +0 -2
- package/dist/assets/RequiresTab-B53DZ6Dn.js.map +0 -1
- package/dist/assets/RoutingView-B5gwI-1h.js +0 -9
- package/dist/assets/RoutingView-B5gwI-1h.js.map +0 -1
- package/dist/assets/RuntimeTab-B9lDbflh.js +0 -3
- package/dist/assets/RuntimeTab-B9lDbflh.js.map +0 -1
- package/dist/assets/SelectMenu.vue_vue_type_script_setup_true_lang-JY8m6tfk.js +0 -2
- package/dist/assets/SettingsView-DGor_LCJ.js +0 -2
- package/dist/assets/SettingsView-DGor_LCJ.js.map +0 -1
- package/dist/assets/SpecDetailView-B0IAuWzl.js +0 -2
- package/dist/assets/SpecDetailView-B0IAuWzl.js.map +0 -1
- package/dist/assets/SpecsView-BTD2qo9l.js +0 -2
- package/dist/assets/SpecsView-BTD2qo9l.js.map +0 -1
- package/dist/assets/SsoView-AzBjKwF_.css +0 -1
- package/dist/assets/SsoView-B_GrAR_X.js +0 -2
- package/dist/assets/SsoView-B_GrAR_X.js.map +0 -1
- package/dist/assets/SubmitView-D5UffFTS.js +0 -50
- package/dist/assets/SubmitView-D5UffFTS.js.map +0 -1
- package/dist/assets/SwarmView-BG2DN4OG.js +0 -2
- package/dist/assets/SwarmView-BG2DN4OG.js.map +0 -1
- package/dist/assets/Teleport-DwPgOPS3.js +0 -4
- package/dist/assets/Teleport-DwPgOPS3.js.map +0 -1
- package/dist/assets/TerminalTab-BLjzwPsG.js +0 -3
- package/dist/assets/TerminalTab-BLjzwPsG.js.map +0 -1
- package/dist/assets/UsersView-BOGuCj7S.js +0 -2
- package/dist/assets/UsersView-BOGuCj7S.js.map +0 -1
- package/dist/assets/UsersView-Bd8B9pLD.css +0 -1
- package/dist/assets/VariablesView-C5XVfRcx.js +0 -3
- package/dist/assets/VariablesView-C5XVfRcx.js.map +0 -1
- package/dist/assets/VariablesView-Ck3IyRCN.css +0 -1
- package/dist/assets/index-B1ONXQzZ.css +0 -1
- package/dist/assets/index-BzdU0RNE.js +0 -159
- package/dist/assets/index-BzdU0RNE.js.map +0 -1
- package/dist/assets/useDeployment-D3f7oVQg.js +0 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import{d as Q,i as c,p as U,N as O,c as a,a as s,t as v,n,F as w,e as y,h as b,f as p,E as Y,g as R,v as E,_ as Z,ah as j,r as F,z as ee,j as N,k as te,o as l,m as se,J as oe,a6 as le,C as ae,ae as M}from"./index-D_Ks6aQi.js";import{a as ne,w as ie,p as q,r as re}from"./useVars-VN9gYdR6.js";import{A as ue}from"./ActionButton-OewKsFrK.js";import{_ as de}from"./ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js";import{_ as pe}from"./VarEditor.vue_vue_type_script_setup_true_lang-BNQTEru_.js";import{_ as ve}from"./SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";import"./VarIO-BFBaI8oU.js";const me={class:"page-head"},ce={class:"sub"},ye={class:"panel"},ke={key:0,class:"banner warn"},fe={key:0},he={key:1},be={key:2},ge={key:3},we={key:2,class:"banner warn"},Se={class:"row",style:{"margin-bottom":"var(--s4)"}},Ce={class:"field",style:{"max-width":"280px"}},xe={class:"hint mute"},$e={key:3,class:"row",style:{"margin-bottom":"var(--s4)"}},Ve={class:"check"},Re={class:"check"},Ne={class:"field",style:{"max-width":"340px"}},Ae={key:0,class:"hint"},Te={key:0},Le={key:1},Ue={key:2},Ee={class:"field"},Ie={class:"field",style:{"margin-top":"var(--s3)"}},Pe={class:"row",style:{"margin-top":"var(--s5)"}},We={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},De={key:1,class:"s-ok"},Oe={key:6,class:"banner warn"},je={key:0},Fe={key:7,class:"banner warn"},Me=`services:
|
|
2
|
+
app:
|
|
3
|
+
image: nginx:alpine
|
|
4
|
+
profiles: [app]
|
|
5
|
+
labels:
|
|
6
|
+
# The ONE label routing needs: pstack generates the Traefik router, TLS and network wiring
|
|
7
|
+
# from it. Writing any traefik.* label yourself is the opt-out — pstack then leaves the
|
|
8
|
+
# service entirely alone, so only do that when you need something the shorthand cannot say.
|
|
9
|
+
- pstack.routing.port=80
|
|
10
|
+
`,Ge=Q({__name:"SubmitView",props:{id:{}},setup(I){const A=I,o=c({id:A.id??"",spec:"",compose:""}),k=c([{k:"",v:""}]),f=c("inline"),S=c(""),C=c(!1),g=c(""),r=c(null),x=c([]),z=N(()=>[{value:"",label:"— choose —"},...x.value.map(t=>({value:t.name,label:t.name,hint:t.kind}))]);U.get("/api/specs").then(t=>{t.ok&&Array.isArray(t.body.specs)&&(x.value=t.body.specs)});const $=N(()=>x.value.find(t=>t.name===S.value)),T=N(()=>!!A.id),B=se(),V=N(()=>{const t=B.query.from;return typeof t=="string"&&t?t:null}),m=c("idle"),h=c(null);async function P(t){m.value="loading",h.value=null;const e=await U.getAuthed(`/api/deployments/${encodeURIComponent(t)}/source`);if(e.status===404||!e.ok){m.value="failed";return}if(e.body.sourceWithheld){m.value="withheld",e.body.specName&&(h.value=e.body.specName);return}o.value.spec||(o.value.spec=e.body.spec??""),o.value.compose||(o.value.compose=e.body.compose??""),e.body.specName&&(h.value=e.body.specName),m.value="loaded"}O(V,async t=>{var u;if(!t||A.id)return;g.value="",r.value=null,o.value.spec="",o.value.compose="";const e=(u=M(t))==null?void 0:u.vars;e&&(k.value=q(e)),await P(t),o.value.id=""},{immediate:!0}),O(()=>A.id,t=>{var d;if(g.value="",r.value=null,m.value="idle",h.value=null,!t)return;t!==o.value.id&&(o.value.spec="",o.value.compose=""),o.value.id=t;const e=(d=M(t))==null?void 0:d.vars,u=e?q(e):re(t);u.some(L=>L.k)&&(k.value=u),P(t)},{immediate:!0});const K=N(()=>!!o.value.id&&(f.value==="stored"?!!S.value:!!o.value.spec.trim())&&!C.value);async function _(){C.value=!0,g.value="",r.value=null;const t=o.value.id,e=ne(k.value),u={vars:e,env:e};f.value==="stored"?u.specName=S.value:(u.spec=o.value.spec,o.value.compose.trim()&&(u.compose=o.value.compose));const d=await U.put(`/api/deployments/${encodeURIComponent(t)}`,u);if(C.value=!1,d.status===200||d.status===201){r.value={id:d.body.id??t,stack:d.body.stack??"?",created:d.status===201,stackSharedWith:d.body.stackSharedWith,swarmNotes:d.body.swarmNotes},ie(t,k.value),oe(),le("ok",`${d.status===201?"Created":"Replaced"} ${t} → ${r.value.stack}`,{to:`/deployments/${encodeURIComponent(t)}`,toLabel:"Open"});return}g.value=ae(d,"save this deployment")}function H(){const t=location.hostname;if(t==="localhost"||t.endsWith(".localhost")||/^[\d.]+$/.test(t)||t.includes(":"))return"preview.example.com";const e=t.split(".");return e.length>=3?e.slice(1).join("."):t}const J=t=>`version: 1
|
|
11
|
+
kind: isolated
|
|
12
|
+
|
|
13
|
+
# The stack identity: the compose project name, and $STACK inside every hook.
|
|
14
|
+
# \${PR} is interpolated ONCE, at resolve time, from the variables sent with the request.
|
|
15
|
+
stack: pr-\${PR}
|
|
16
|
+
|
|
17
|
+
env:
|
|
18
|
+
# The base every generated hostname hangs off: the app below is served at
|
|
19
|
+
# app-pr-\${PR}.${t}
|
|
20
|
+
PREVIEW_DOMAIN: ${t}
|
|
21
|
+
|
|
22
|
+
compose:
|
|
23
|
+
file: compose.yml
|
|
24
|
+
# Every service sits behind a profile so a bare \`up\` starts nothing.
|
|
25
|
+
# \`down\` always enables ALL of them — otherwise the unselected profiles' network leaks.
|
|
26
|
+
profiles: [app]
|
|
27
|
+
|
|
28
|
+
requires:
|
|
29
|
+
- name: traefik-network
|
|
30
|
+
assert: docker network inspect traefik >/dev/null 2>&1
|
|
31
|
+
hint: run \`pstack init\` on the host first
|
|
32
|
+
|
|
33
|
+
axes:
|
|
34
|
+
# Hooks run from the deployment directory, which holds only spec.yml and compose.yml —
|
|
35
|
+
# so use inline shell or absolute paths, never ./hooks/foo.sh.
|
|
36
|
+
- name: scratch
|
|
37
|
+
up: mkdir -p /tmp/$STACK
|
|
38
|
+
assert_live: test -d /tmp/$STACK
|
|
39
|
+
down: rm -rf /tmp/$STACK
|
|
40
|
+
# assert_gone MUST FAIL CLOSED. A bare \`! <probe>\` reports "gone" whenever the probe fails
|
|
41
|
+
# for any reason — a missing CLI, an expired token — turning "I could not tell" into
|
|
42
|
+
# "it is gone", a false negative on the one thing this tool exists to catch.
|
|
43
|
+
assert_gone: |
|
|
44
|
+
test -d /tmp || exit 1
|
|
45
|
+
! test -d /tmp/$STACK
|
|
46
|
+
`;function X(){f.value="inline",o.value.spec=J(H()),o.value.compose.trim()||(o.value.compose=Me),o.value.id||(o.value.id="pr-123"),k.value.some(t=>t.k)||(k.value=[{k:"PR",v:"123"}])}return(t,e)=>{var d,L,W,D;const u=te("RouterLink");return l(),a("div",null,[s("div",me,[s("div",null,[s("h1",null,v(T.value?"Edit a deployment":V.value?"Duplicate a deployment":"Submit a deployment"),1),s("div",ce,v(T.value?"Saving replaces the record":V.value?`Copied from ${V.value} — give it a new id`:"Store a spec, then deploy it"),1)]),e[7]||(e[7]=s("span",{class:"grow"},null,-1)),s("button",{class:"ghost",onClick:X},"Load example spec")]),s("section",ye,[V.value&&!T.value?(l(),a("div",ke,[e[15]||(e[15]=s("b",null,"Change the stack, not just the id.",-1)),s("p",null,[e[10]||(e[10]=n(" Two deployments on one ",-1)),e[11]||(e[11]=s("code",null,"stack",-1)),e[12]||(e[12]=n(" share a compose project — ",-1)),e[13]||(e[13]=s("code",null,"down",-1)),e[14]||(e[14]=n(" on either stops both. ",-1)),m.value==="withheld"?(l(),a(w,{key:0},[y(u,{to:"/settings"},{default:b(()=>[...e[8]||(e[8]=[n("Add a token",-1)])]),_:1}),e[9]||(e[9]=n(" to copy the spec. ",-1))],64)):p("",!0)])])):p("",!0),T.value?(l(),a("div",{key:1,class:Y(["banner",m.value==="loaded"?"info":"warn"])},[s("b",null,"Replacing "+v(I.id)+".",1),m.value==="loading"?(l(),a("p",fe,"Loading…")):m.value==="loaded"?(l(),a("p",he," Saving replaces the record outright — anything you delete stops being tracked but keeps running. ")):m.value==="withheld"?(l(),a("p",be,[e[17]||(e[17]=n(" Stored spec withheld — ",-1)),y(u,{to:"/settings"},{default:b(()=>[...e[16]||(e[16]=[n("add your token",-1)])]),_:1}),e[18]||(e[18]=n(", or paste the complete spec. Anything you leave out stops being tracked but keeps running. ",-1))])):(l(),a("p",ge," Stored spec unavailable. Paste the complete spec — anything you leave out stops being tracked but keeps running. "))],2)):p("",!0),h.value?(l(),a("div",we,[s("b",null,"Tracks the stored spec “"+v(h.value)+"”.",1),s("p",null,[e[20]||(e[20]=n(" Saving forks it — ",-1)),y(u,{to:`/specs/${encodeURIComponent(h.value)}`},{default:b(()=>[...e[19]||(e[19]=[n("edit the spec",-1)])]),_:1},8,["to"]),e[21]||(e[21]=n(" to change every deployment using it. ",-1))])])):p("",!0),s("div",Se,[s("div",Ce,[e[24]||(e[24]=s("label",{for:"did"},"Deployment id",-1)),R(s("input",{id:"did","onUpdate:modelValue":e[0]||(e[0]=i=>o.value.id=i),type:"text",placeholder:"pr-123",spellcheck:"false",autocomplete:"off"},null,512),[[E,o.value.id,void 0,{trim:!0}]]),s("span",xe,[e[23]||(e[23]=n(" Lower case ",-1)),y(Z,{label:"allowed characters in a deployment id"},{default:b(()=>[...e[22]||(e[22]=[s("code",null,"[a-z0-9][a-z0-9._-]{0,63}",-1)])]),_:1})])])]),x.value.length?(l(),a("div",$e,[s("label",Ve,[R(s("input",{"onUpdate:modelValue":e[1]||(e[1]=i=>f.value=i),type:"radio",value:"inline"},null,512),[[j,f.value]]),e[25]||(e[25]=n(" inline spec ",-1))]),s("label",Re,[R(s("input",{"onUpdate:modelValue":e[2]||(e[2]=i=>f.value=i),type:"radio",value:"stored"},null,512),[[j,f.value]]),e[26]||(e[26]=n(" reference a stored spec ",-1))])])):p("",!0),f.value==="stored"&&x.value.length?(l(),a(w,{key:4},[s("div",Ne,[e[27]||(e[27]=s("label",{for:"sn"},"Stored spec",-1)),y(ve,{modelValue:S.value,"onUpdate:modelValue":e[3]||(e[3]=i=>S.value=i),id:"sn",label:"Stored spec",options:z.value},null,8,["modelValue","options"])]),$.value?(l(),a("p",Ae,[$.value.description?(l(),a("span",Te,v($.value.description)+" — ",1)):p("",!0),e[28]||(e[28]=n(" needs ",-1)),$.value.requiredVars.length?(l(),a("b",Le,v($.value.requiredVars.join(", ")),1)):(l(),a("span",Ue,"no variables")),e[29]||(e[29]=n(". ",-1))])):p("",!0)],64)):(l(),a(w,{key:5},[s("div",Ee,[e[30]||(e[30]=s("label",{for:"spec"},"Spec.yml",-1)),R(s("textarea",{id:"spec","onUpdate:modelValue":e[4]||(e[4]=i=>o.value.spec=i),rows:"12",spellcheck:"false",placeholder:`version: 1
|
|
47
|
+
kind: isolated
|
|
48
|
+
stack: pr-\${PR}
|
|
49
|
+
axes: []`},null,512),[[E,o.value.spec]])]),s("div",Ie,[e[31]||(e[31]=s("label",{for:"compose"},[n("compose.yml "),s("span",{class:"mute"},"(optional)")],-1)),R(s("textarea",{id:"compose","onUpdate:modelValue":e[5]||(e[5]=i=>o.value.compose=i),rows:"6",spellcheck:"false",placeholder:"Written next to spec.yml"},null,512),[[E,o.value.compose]])]),e[32]||(e[32]=s("p",{class:"hint"},"Hooks must be inline shell or an absolute path.",-1))],64)),e[40]||(e[40]=s("h2",{class:"section",style:{margin:"var(--s5) 0 var(--s3)"}},"Variables",-1)),y(pe,{modelValue:k.value,"onUpdate:modelValue":e[6]||(e[6]=i=>k.value=i)},null,8,["modelValue"]),e[41]||(e[41]=s("p",{class:"hint"},"Saved with the deployment, so teardown targets the same stack.",-1)),s("div",Pe,[y(ue,{variant:"primary",pending:C.value,disabled:!K.value,onClick:_},{default:b(()=>[n(v(C.value?"Submitting…":"Submit"),1)]),_:1},8,["pending","disabled"]),r.value?p("",!0):(l(),a("span",We," Nothing is saved until the spec is valid ")),r.value?(l(),a("span",De,[n(v(r.value.created?"Created":"Replaced")+" — stack ",1),s("b",null,v(r.value.stack),1),y(u,{to:`/deployments/${encodeURIComponent(r.value.id)}`,style:{"margin-left":"8px"}},{default:b(()=>[...e[33]||(e[33]=[n(" open → ",-1)])]),_:1},8,["to"])])):p("",!0)]),(L=(d=r.value)==null?void 0:d.stackSharedWith)!=null&&L.length?(l(),a("div",Oe,[s("b",null,"Stack “"+v(r.value.stack)+"” is already in use.",1),s("p",null,[(l(!0),a(w,null,F(r.value.stackSharedWith,(i,G)=>(l(),a(w,{key:i},[y(u,{to:`/deployments/${encodeURIComponent(i)}`},{default:b(()=>[n(v(i),1)]),_:2},1032,["to"]),G<r.value.stackSharedWith.length-1?(l(),a("span",je,", ")):p("",!0)],64))),128)),e[34]||(e[34]=n("— ",-1)),e[35]||(e[35]=s("code",null,"down",-1)),e[36]||(e[36]=n(" on either stops both. Change this one's ",-1)),e[37]||(e[37]=s("code",null,"stack",-1)),e[38]||(e[38]=n(" if that was not intended. ",-1))])])):p("",!0),(D=(W=r.value)==null?void 0:W.swarmNotes)!=null&&D.length?(l(),a("div",Fe,[e[39]||(e[39]=s("b",null,"Swarm will change this compose file.",-1)),s("ul",null,[(l(!0),a(w,null,F(r.value.swarmNotes,i=>(l(),a("li",{key:i},v(i),1))),128))])])):p("",!0),g.value?(l(),ee(de,{key:8,text:g.value,title:"The server rejected this submission."},null,8,["text"])):p("",!0)])])}}});export{Ge as default};
|
|
50
|
+
//# sourceMappingURL=SubmitView-BcAcTlwC.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SubmitView-BcAcTlwC.js","sources":["../../src/views/SubmitView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Submit or replace a deployment.\n *\n * The server's 400 is the whole point of this screen. It carries the SpecError verbatim — the key\n * or variable that is wrong, often the corrected form, and the assert-lint warnings about an\n * `assert_gone` that fails open. That text is the tool's main teaching surface, so it is rendered\n * whole, with newlines intact, and never summarised.\n *\n * The spec is parsed BEFORE anything touches disk, so a rejected submission leaves nothing behind\n * and, on a replace, a good record is never destroyed over a typo.\n */\nimport { computed, ref, watch } from 'vue';\nimport { useRoute } from 'vue-router';\nimport { api, problem } from '../api/client';\nimport InfoHint from '../components/InfoHint.vue';\nimport type {\n DeploymentSourceResponse,\n SpecMeta,\n SpecsResponse,\n SubmitResponse,\n VarPair,\n} from '../api/types';\nimport { loadDeployments, rowFor } from '../composables/useControlPlane';\nimport { pairsFromRecord, readVars, recordFromPairs, writeVars } from '../composables/useVars';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport VarEditor from '../components/VarEditor.vue';\nimport SelectMenu from '../components/SelectMenu.vue';\n\nconst props = defineProps<{ id?: string }>();\n\nconst form = ref({ id: props.id ?? '', spec: '', compose: '' });\nconst vars = ref<VarPair[]>([{ k: '', v: '' }]);\nconst source = ref<'inline' | 'stored'>('inline');\nconst specName = ref('');\n\nconst submitting = ref(false);\nconst error = ref('');\nconst result = ref<{\n id: string;\n stack: string;\n created: boolean;\n /** Other deployments resolving to the same stack — reported by the server on a new deployment. */\n stackSharedWith?: string[];\n /** Compose keys swarm will ignore — reported by the server at submit time. */\n swarmNotes?: string[];\n} | null>(null);\n\n/**\n * Stored specs, when this server has them. A build without `/api/specs` answers 404, which is not\n * an error here — the picker simply is not offered and inline stays the only shape.\n */\nconst specs = ref<SpecMeta[]>([]);\n\n/** `— choose —` stays first: an empty value is a real state here, not a placeholder. */\nconst specOptions = computed(() => [\n { value: '', label: '— choose —' },\n ...specs.value.map((s) => ({ value: s.name, label: s.name, hint: s.kind })),\n]);\nvoid api.get<SpecsResponse>('/api/specs').then((r) => {\n if (r.ok && Array.isArray(r.body.specs)) specs.value = r.body.specs;\n});\n\nconst chosenSpec = computed(() => specs.value.find((s) => s.name === specName.value));\n\nconst replacing = computed(() => !!props.id);\n\n/**\n * Duplicate mode: `/submit?from=<id>` copies an existing deployment's spec, compose and variables into\n * a NEW submission. Distinct from replacing — the id starts empty so you name the new one, and nothing\n * is written over.\n *\n * The `stack:` line is the thing to watch, which is why the banner says so: copy a spec whose stack is a\n * literal and both deployments drive the same compose project, so `down` on either stops the other's\n * containers. A spec whose stack interpolates a variable (`pr-${PR}`) just needs a different value. The\n * server independently reports a collision at submit time — see `stackSharedWith`.\n */\nconst route = useRoute();\nconst copyFrom = computed(() => {\n const v = route.query.from;\n return typeof v === 'string' && v ? v : null;\n});\n\n/**\n * Replacing now PRE-FILLS from what is stored.\n *\n * It used to start empty, because nothing could hand the source back. Replacing is whole-record, so\n * an operator retyping a spec from memory silently dropped whatever they forgot — and a dropped axis\n * stops being tracked while the resources it created keep running. Being unable to read your own\n * submission is what made that a likely mistake rather than a careless one.\n *\n * The body is still cleared FIRST, on every id change. A leftover body from another deployment is the\n * dangerous case: it is perfectly valid YAML, so the PUT would succeed and overwrite the wrong\n * deployment with nothing able to catch it. Clearing first means a failed or withheld fetch leaves the\n * form empty rather than holding someone else's spec.\n */\nconst sourceState = ref<'idle' | 'loading' | 'loaded' | 'withheld' | 'failed'>('idle');\n/** Set when this deployment tracks a stored spec — editing this copy forks it from the original. */\nconst forkWarning = ref<string | null>(null);\n\nasync function loadSource(id: string): Promise<void> {\n sourceState.value = 'loading';\n forkWarning.value = null;\n const r = await api.getAuthed<DeploymentSourceResponse>(\n `/api/deployments/${encodeURIComponent(id)}/source`,\n );\n // A build of the API without the route answers 404; that is a capability difference, not an error,\n // and it must land on the old empty-form behaviour rather than an alarming banner.\n if (r.status === 404 || !r.ok) {\n sourceState.value = 'failed';\n return;\n }\n if (r.body.sourceWithheld) {\n sourceState.value = 'withheld';\n if (r.body.specName) forkWarning.value = r.body.specName;\n return;\n }\n // Only fill if the user has not started typing — a slow fetch must never overwrite their edit.\n if (!form.value.spec) form.value.spec = r.body.spec ?? '';\n if (!form.value.compose) form.value.compose = r.body.compose ?? '';\n if (r.body.specName) forkWarning.value = r.body.specName;\n sourceState.value = 'loaded';\n}\n\n// Duplicate: same fetch, but the id stays empty and the variables come across too, since a copied\n// spec interpolates the same names.\nwatch(\n copyFrom,\n async (from) => {\n if (!from || props.id) return;\n error.value = '';\n result.value = null;\n form.value.spec = '';\n form.value.compose = '';\n const saved = rowFor(from)?.vars;\n if (saved) vars.value = pairsFromRecord(saved);\n await loadSource(from);\n // A copied record's own id is not a name for the new one; the operator picks that.\n form.value.id = '';\n },\n { immediate: true },\n);\n\nwatch(\n () => props.id,\n (id) => {\n error.value = '';\n result.value = null;\n sourceState.value = 'idle';\n forkWarning.value = null;\n if (!id) return;\n if (id !== form.value.id) {\n form.value.spec = '';\n form.value.compose = '';\n }\n form.value.id = id;\n // Seed the variables from what this deployment already uses: replacing a spec usually needs\n // the same ones that resolved it.\n const stored = rowFor(id)?.vars;\n const saved = stored ? pairsFromRecord(stored) : readVars(id);\n if (saved.some((e) => e.k)) vars.value = saved;\n void loadSource(id);\n },\n { immediate: true },\n);\n\nconst canSubmit = computed(\n () =>\n !!form.value.id &&\n (source.value === 'stored' ? !!specName.value : !!form.value.spec.trim()) &&\n !submitting.value,\n);\n\nasync function submit(): Promise<void> {\n submitting.value = true;\n error.value = '';\n result.value = null;\n\n const id = form.value.id;\n const record = recordFromPairs(vars.value);\n /**\n * `vars` are STORED with the deployment on servers that support it, which makes a later `down`\n * resolve the same stack `up` created with no query parameters at all. `env` validates this\n * submission on every server, new or old. Sending both is what keeps this form working against\n * either build — an older server ignores the key it does not know.\n */\n const body: Record<string, unknown> = { vars: record, env: record };\n if (source.value === 'stored') body.specName = specName.value;\n else {\n body.spec = form.value.spec;\n if (form.value.compose.trim()) body.compose = form.value.compose;\n }\n\n const r = await api.put<SubmitResponse>(`/api/deployments/${encodeURIComponent(id)}`, body);\n submitting.value = false;\n\n if (r.status === 200 || r.status === 201) {\n result.value = {\n id: r.body.id ?? id,\n stack: r.body.stack ?? '?',\n created: r.status === 201,\n stackSharedWith: r.body.stackSharedWith,\n swarmNotes: r.body.swarmNotes,\n };\n // Persist the variables that just validated this spec. On an older server this is the single\n // thing keeping a later `down` pointed at the stack the `up` created.\n writeVars(id, vars.value);\n void loadDeployments();\n toast('ok', `${r.status === 201 ? 'Created' : 'Replaced'} ${id} → ${result.value.stack}`, {\n to: `/deployments/${encodeURIComponent(id)}`,\n toLabel: 'Open',\n });\n return;\n }\n // 400 (SpecError), 409 (a job in flight over this stack), 401, 404 — all rendered verbatim.\n error.value = problem(r, 'save this deployment');\n}\n\n/**\n * The preview domain, guessed from where this page is served.\n *\n * The UI lives on a host UNDER the preview domain (ui.preview.example.com, pstack.preview…), so\n * previews live beside it: drop this page's own first label and that is the base every generated\n * hostname hangs off. localhost and bare IPs have no domain to offer — those keep an honest\n * placeholder rather than generating `pr-123.192.168.1.5`.\n */\nfunction detectPreviewDomain(): string {\n const h = location.hostname;\n if (h === 'localhost' || h.endsWith('.localhost') || /^[\\d.]+$/.test(h) || h.includes(':')) {\n return 'preview.example.com';\n }\n const labels = h.split('.');\n return labels.length >= 3 ? labels.slice(1).join('.') : h;\n}\n\n/**\n * Kept as a function of the domain so the example is true on the host it is loaded on — a spec\n * copied from here should deploy without edits, not after find-and-replacing example.com.\n *\n * The compose half uses `pstack.routing.port`, NOT hand-written traefik labels: pstack generates\n * the router, TLS and network wiring from that one label. A bare `traefik.enable=true` is the\n * OPT-OUT signal (any traefik.* label means \"leave my service alone\") — which is what an older\n * version of this example taught, producing a service that was never reachable.\n */\nconst exampleSpec = (domain: string): string => `version: 1\nkind: isolated\n\n# The stack identity: the compose project name, and $STACK inside every hook.\n# \\${PR} is interpolated ONCE, at resolve time, from the variables sent with the request.\nstack: pr-\\${PR}\n\nenv:\n # The base every generated hostname hangs off: the app below is served at\n # app-pr-\\${PR}.${domain}\n PREVIEW_DOMAIN: ${domain}\n\ncompose:\n file: compose.yml\n # Every service sits behind a profile so a bare \\`up\\` starts nothing.\n # \\`down\\` always enables ALL of them — otherwise the unselected profiles' network leaks.\n profiles: [app]\n\nrequires:\n - name: traefik-network\n assert: docker network inspect traefik >/dev/null 2>&1\n hint: run \\`pstack init\\` on the host first\n\naxes:\n # Hooks run from the deployment directory, which holds only spec.yml and compose.yml —\n # so use inline shell or absolute paths, never ./hooks/foo.sh.\n - name: scratch\n up: mkdir -p /tmp/$STACK\n assert_live: test -d /tmp/$STACK\n down: rm -rf /tmp/$STACK\n # assert_gone MUST FAIL CLOSED. A bare \\`! <probe>\\` reports \"gone\" whenever the probe fails\n # for any reason — a missing CLI, an expired token — turning \"I could not tell\" into\n # \"it is gone\", a false negative on the one thing this tool exists to catch.\n assert_gone: |\n test -d /tmp || exit 1\n ! test -d /tmp/$STACK\n`;\n\nconst EXAMPLE_COMPOSE = `services:\n app:\n image: nginx:alpine\n profiles: [app]\n labels:\n # The ONE label routing needs: pstack generates the Traefik router, TLS and network wiring\n # from it. Writing any traefik.* label yourself is the opt-out — pstack then leaves the\n # service entirely alone, so only do that when you need something the shorthand cannot say.\n - pstack.routing.port=80\n`;\n\nfunction loadExample(): void {\n source.value = 'inline';\n form.value.spec = exampleSpec(detectPreviewDomain());\n if (!form.value.compose.trim()) form.value.compose = EXAMPLE_COMPOSE;\n if (!form.value.id) form.value.id = 'pr-123';\n if (!vars.value.some((e) => e.k)) vars.value = [{ k: 'PR', v: '123' }];\n}\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>\n {{ replacing ? 'Edit a deployment' : copyFrom ? 'Duplicate a deployment' : 'Submit a deployment' }}\n </h1>\n <div class=\"sub\">\n {{\n replacing\n ? 'Saving replaces the record'\n : copyFrom\n ? `Copied from ${copyFrom} — give it a new id`\n : 'Store a spec, then deploy it'\n }}\n </div>\n </div>\n <span class=\"grow\" />\n <button class=\"ghost\" @click=\"loadExample\">Load example spec</button>\n </div>\n\n <section class=\"panel\">\n <!-- The \"nothing saved until valid\" reassurance lives beside the Submit button below — the\n moment of commitment is where a guarantee about committing belongs, not floating above\n an unrelated id field. -->\n\n <!--\n The one thing that makes duplicating dangerous, said before the form rather than after the\n submission: two records resolving to one stack drive the same compose project.\n -->\n <div v-if=\"copyFrom && !replacing\" class=\"banner warn\">\n <b>Change the stack, not just the id.</b>\n <p>\n Two deployments on one <code>stack</code> share a compose project — <code>down</code> on\n either stops both.\n <template v-if=\"sourceState === 'withheld'\">\n <RouterLink to=\"/settings\">Add a token</RouterLink> to copy the spec.\n </template>\n </p>\n </div>\n\n <!--\n A `PUT` replaces the whole record. The four source states stay distinct: withheld (no token)\n is not the same as failed (no route, or an error), and neither is an empty form by choice.\n -->\n <div v-if=\"replacing\" class=\"banner\" :class=\"sourceState === 'loaded' ? 'info' : 'warn'\">\n <b>Replacing {{ id }}.</b>\n <p v-if=\"sourceState === 'loading'\">Loading…</p>\n <p v-else-if=\"sourceState === 'loaded'\">\n Saving replaces the record outright — anything you delete stops being tracked but keeps\n running.\n </p>\n <p v-else-if=\"sourceState === 'withheld'\">\n Stored spec withheld — <RouterLink to=\"/settings\">add your token</RouterLink>, or paste the\n complete spec. Anything you leave out stops being tracked but keeps running.\n </p>\n <p v-else>\n Stored spec unavailable. Paste the complete spec — anything you leave out stops being\n tracked but keeps running.\n </p>\n </div>\n\n <!--\n A deployment that references a stored spec keeps its own copy of the source. Editing that copy\n forks it: the stored spec every other deployment shares is untouched, and this one stops\n following it. Silent forks are the kind of thing discovered months later.\n -->\n <div v-if=\"forkWarning\" class=\"banner warn\">\n <b>Tracks the stored spec “{{ forkWarning }}”.</b>\n <p>\n Saving forks it —\n <RouterLink :to=\"`/specs/${encodeURIComponent(forkWarning)}`\">edit the spec</RouterLink>\n to change every deployment using it.\n </p>\n </div>\n\n <div class=\"row\" style=\"margin-bottom: var(--s4)\">\n <div class=\"field\" style=\"max-width: 280px\">\n <label for=\"did\">Deployment id</label>\n <input\n id=\"did\"\n v-model.trim=\"form.id\"\n type=\"text\"\n placeholder=\"pr-123\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n <span class=\"hint mute\">\n Lower case\n <InfoHint label=\"allowed characters in a deployment id\">\n <code>[a-z0-9][a-z0-9._-]{0,63}</code>\n </InfoHint>\n </span>\n </div>\n </div>\n\n <!-- Offered only when this server actually has stored specs. -->\n <div v-if=\"specs.length\" class=\"row\" style=\"margin-bottom: var(--s4)\">\n <label class=\"check\">\n <input v-model=\"source\" type=\"radio\" value=\"inline\" /> inline spec\n </label>\n <label class=\"check\">\n <input v-model=\"source\" type=\"radio\" value=\"stored\" /> reference a stored spec\n </label>\n </div>\n\n <template v-if=\"source === 'stored' && specs.length\">\n <div class=\"field\" style=\"max-width: 340px\">\n <label for=\"sn\">Stored spec</label>\n <SelectMenu v-model=\"specName\" id=\"sn\" label=\"Stored spec\" :options=\"specOptions\" />\n </div>\n <p v-if=\"chosenSpec\" class=\"hint\">\n <span v-if=\"chosenSpec.description\">{{ chosenSpec.description }} — </span>\n needs\n <b v-if=\"chosenSpec.requiredVars.length\">{{ chosenSpec.requiredVars.join(', ') }}</b>\n <span v-else>no variables</span>.\n </p>\n </template>\n\n <template v-else>\n <div class=\"field\">\n <label for=\"spec\">Spec.yml</label>\n <textarea\n id=\"spec\"\n v-model=\"form.spec\"\n rows=\"12\"\n spellcheck=\"false\"\n placeholder=\"version: 1 kind: isolated stack: pr-${PR} axes: []\"\n />\n </div>\n\n <div class=\"field\" style=\"margin-top: var(--s3)\">\n <label for=\"compose\">compose.yml <span class=\"mute\">(optional)</span></label>\n <textarea\n id=\"compose\"\n v-model=\"form.compose\"\n rows=\"6\"\n spellcheck=\"false\"\n placeholder=\"Written next to spec.yml\"\n />\n </div>\n <p class=\"hint\">Hooks must be inline shell or an absolute path.</p>\n </template>\n\n <h2 class=\"section\" style=\"margin: var(--s5) 0 var(--s3)\">Variables</h2>\n <VarEditor v-model=\"vars\" />\n <p class=\"hint\">Saved with the deployment, so teardown targets the same stack.</p>\n\n <div class=\"row\" style=\"margin-top: var(--s5)\">\n <ActionButton variant=\"primary\" :pending=\"submitting\" :disabled=\"!canSubmit\" @click=\"submit\">\n {{ submitting ? 'Submitting…' : 'Submit' }}\n </ActionButton>\n <span v-if=\"!result\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n Nothing is saved until the spec is valid\n </span>\n <span v-if=\"result\" class=\"s-ok\">\n {{ result.created ? 'Created' : 'Replaced' }} — stack\n <b>{{ result.stack }}</b>\n <RouterLink :to=\"`/deployments/${encodeURIComponent(result.id)}`\" style=\"margin-left: 8px\">\n open →\n </RouterLink>\n </span>\n </div>\n\n <!--\n The server's rejection text IS the documentation: it names the key, the variable or the\n assert that is wrong, and often prints the corrected form. Rendered whole, newlines and\n indentation intact. Never truncate it, never collapse it, never say \"invalid spec\".\n -->\n <!--\n Reported by the server, which resolved every other deployment to check. Shown after the fact\n because it can only be known once the spec has resolved — and it is a warning, not a rejection:\n it can be deliberate, and refusing over a guess would be worse than saying so.\n -->\n <div v-if=\"result?.stackSharedWith?.length\" class=\"banner warn\">\n <b>Stack “{{ result.stack }}” is already in use.</b>\n <p>\n <template v-for=\"(o, i) in result.stackSharedWith\" :key=\"o\">\n <RouterLink :to=\"`/deployments/${encodeURIComponent(o)}`\">{{ o }}</RouterLink\n ><span v-if=\"i < result.stackSharedWith.length - 1\">, </span> </template\n >— <code>down</code> on either stops both. Change this one's <code>stack</code> if that was\n not intended.\n </p>\n </div>\n\n <!--\n The keys swarm drops. Same reasoning as the banner above: advisory, and only knowable once\n the spec resolved. It is shown here rather than left to the deploy log because the whole\n point is to reach the author while they are still looking at the file they just wrote.\n -->\n <div v-if=\"result?.swarmNotes?.length\" class=\"banner warn\">\n <b>Swarm will change this compose file.</b>\n <ul>\n <li v-for=\"n in result.swarmNotes\" :key=\"n\">{{ n }}</li>\n </ul>\n </div>\n\n <ErrorNote v-if=\"error\" :text=\"error\" title=\"The server rejected this submission.\" />\n </section>\n </div>\n</template>\n"],"names":["EXAMPLE_COMPOSE","props","__props","form","ref","vars","source","specName","submitting","error","result","specs","specOptions","computed","s","api","r","chosenSpec","replacing","route","useRoute","copyFrom","v","sourceState","forkWarning","loadSource","id","watch","from","saved","_a","rowFor","pairsFromRecord","stored","readVars","e","canSubmit","submit","record","recordFromPairs","body","writeVars","loadDeployments","toast","problem","detectPreviewDomain","h","labels","exampleSpec","domain","loadExample","_createElementBlock","_createElementVNode","_hoisted_1","_toDisplayString","_hoisted_2","_hoisted_3","_openBlock","_hoisted_4","_cache","_Fragment","_createVNode","_component_RouterLink","_normalizeClass","_hoisted_7","_hoisted_9","_hoisted_10","_hoisted_11","$event","_hoisted_12","InfoHint","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","SelectMenu","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_21","_vModelText","_hoisted_22","VarEditor","_hoisted_23","ActionButton","_hoisted_24","_hoisted_25","_b","_hoisted_26","o","i","_d","_c","_hoisted_28","_renderList","n","_createBlock","ErrorNote"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{d as U,x as E,Q as I,c as s,a as e,e as b,z as y,f as r,n as f,t as n,F as k,r as S,h as R,i as d,a6 as V,o as t,D as H,E as $,u as w,M as _,p as D,C as M}from"./index-D_Ks6aQi.js";import{A as O}from"./ActionButton-OewKsFrK.js";import{_ as A}from"./ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js";import{_ as P}from"./RefreshButton.vue_vue_type_script_setup_true_lang-CQvD4Out.js";import{_ as N}from"./SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js";import{_ as q}from"./SkeletonList.vue_vue_type_script_setup_true_lang-D4pkKZAr.js";import"./_plugin-vue_export-helper-DlAUqK2U.js";const J={class:"page-head"},L={key:2,class:"panel"},Q={class:"phead"},G={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},K={class:"mono"},W={key:0,class:"banner warn"},X={key:1,class:"banner plain"},Y={key:0,class:"banner warn"},Z={class:"mono"},ee={key:1,role:"table",class:"cards"},ae={role:"rowgroup",class:"stagger"},oe={role:"cell",class:"name","data-label":"hostname"},te={key:0,class:"badge info",title:"the node this control plane runs on"},se={role:"cell","data-label":"role"},le={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},ne={role:"cell","data-label":"status"},re={role:"cell","data-label":"availability"},ie={role:"cell","data-label":"engine",class:"mono"},de={role:"cell","data-label":"id",class:"mono mute"},ue={key:2,class:"mute"},me={key:3,class:"panel"},ve={class:"kvlist",style:{"margin-bottom":"var(--s4)"}},ce={class:"k mono"},pe={class:"v mute"},be={class:"row",style:{"flex-wrap":"wrap",gap:"var(--s3)"}},fe={class:"field inline"},he={key:0,class:"field inline"},ye={class:"mute",style:{"font-size":"var(--t-sm)"}},ke={key:1,class:"banner leaked",style:{"margin-top":"var(--s3)"}},we={class:"code",style:{"white-space":"pre-wrap","word-break":"break-all","max-height":"50vh",overflow:"auto"}},_e={class:"row"},De=U({__name:"SwarmView",setup(ge){const l=d(null),c=d(""),g=d(!1);async function x(){const i=await D.get("/api/swarm");if(g.value=!0,!i.ok){c.value=M(i,"read the swarm");return}c.value="",l.value={...i.body,nodes:i.body.nodes??[],ports:i.body.ports??[]}}E(x,1e4);const m=d("command"),h=d("ubuntu"),u=d(""),p=d(!1),v=d(""),C=[{value:"command",label:"docker swarm join …",blurb:"For a machine already running Docker."},{value:"script",label:"Shell script",blurb:"Installs Docker, then joins."},{value:"cloud-config",label:"cloud-config",blurb:"User-data for a fresh machine."},{value:"token",label:"Token only",blurb:"For your own tooling."}];async function z(){p.value=!0,v.value="",u.value="";const i=`format=${m.value}${m.value==="cloud-config"?`&distro=${encodeURIComponent(h.value)}`:""}`,a=await D.getText(`/api/swarm/join?${i}`);if(p.value=!1,a.status===403){v.value="The join token is admin-only.";return}if(a.status===409){v.value="This host is not a swarm manager.";return}if(!a.ok){v.value=a.text||`HTTP ${a.status}`;return}u.value=a.text}async function B(){try{await navigator.clipboard.writeText(u.value),V("ok","Copied.")}catch{V("error","Copy failed — select it and copy by hand.")}}return I(()=>{u.value=""}),(i,a)=>{var T,j;return t(),s("div",null,[e("div",J,[a[3]||(a[3]=e("div",null,[e("h1",null,"Swarm"),e("div",{class:"sub"},"The nodes previews run on")],-1)),a[4]||(a[4]=e("span",{class:"grow"},null,-1)),b(P,{run:x})]),c.value?(t(),y(A,{key:0,text:c.value,title:"Could not read the swarm."},null,8,["text"])):r("",!0),!g.value&&!c.value?(t(),y(q,{key:1,rows:4})):l.value?(t(),s("section",L,[e("div",Q,[a[6]||(a[6]=e("h2",{class:"section"},"Nodes",-1)),a[7]||(a[7]=e("span",{class:"grow"},null,-1)),l.value.active&&l.value.managerAddr?(t(),s("span",G,[a[5]||(a[5]=f(" manager ",-1)),e("span",K,n(l.value.managerAddr),1)])):r("",!0)]),l.value.reachable?l.value.active?(t(),s(k,{key:2},[l.value.error?(t(),s("div",Y,[a[10]||(a[10]=e("b",null,"Docker reported a problem.",-1)),e("p",Z,n(l.value.error),1)])):r("",!0),l.value.nodes.length?(t(),s("table",ee,[a[11]||(a[11]=e("thead",{role:"rowgroup"},[e("tr",{role:"row"},[e("th",{role:"columnheader"},"Hostname"),e("th",{role:"columnheader"},"Role"),e("th",{role:"columnheader"},"Status"),e("th",{role:"columnheader"},"Availability"),e("th",{role:"columnheader"},"Engine"),e("th",{role:"columnheader"},"Id")])],-1)),e("tbody",ae,[(t(!0),s(k,null,S(l.value.nodes,(o,F)=>(t(),s("tr",{key:o.id,role:"row",style:H({"--i":F})},[e("td",oe,[f(n(o.hostname||"—")+" ",1),o.self?(t(),s("span",te,"This node")):r("",!0)]),e("td",se,[e("span",{class:$(["badge",o.role==="manager"?"isolated":""])},n(w(_)(o.role)),3),o.managerStatus?(t(),s("span",le,n(o.managerStatus),1)):r("",!0)]),e("td",ne,[e("span",{class:$(o.status==="ready"?"s-ok":"s-failed")},n(w(_)(o.status)),3)]),e("td",re,n(w(_)(o.availability||"unknown")),1),e("td",ie,n(o.engineVersion||"—"),1),e("td",de,n(o.id.slice(0,12)),1)],4))),128))])])):(t(),s("p",ue,"Docker lists no nodes."))],64)):(t(),s("div",X,[a[9]||(a[9]=e("b",null,"Not a swarm manager.",-1)),e("p",null,n(l.value.note),1)])):(t(),s("div",W,[...a[8]||(a[8]=[e("b",null,"Docker did not answer — this is not the same as no swarm.",-1)])]))])):r("",!0),(T=l.value)!=null&&T.active?(t(),s("section",me,[a[17]||(a[17]=e("div",{class:"phead"},[e("h2",{class:"section"},"Add a worker")],-1)),a[18]||(a[18]=e("p",{class:"dim"},"Open these between the new machine and every other node.",-1)),e("ul",ve,[(t(!0),s(k,null,S(l.value.ports,o=>(t(),s("li",{key:o.port},[e("span",ce,n(o.port),1),e("span",pe,n(o.why),1)]))),128))]),e("div",be,[e("div",fe,[a[12]||(a[12]=e("label",{for:"join-format"},"Format",-1)),b(N,{id:"join-format","model-value":m.value,label:"Join format",options:C.map(o=>({value:o.value,label:o.label})),"onUpdate:modelValue":a[0]||(a[0]=o=>m.value=o)},null,8,["model-value","options"])]),m.value==="cloud-config"?(t(),s("div",he,[a[13]||(a[13]=e("label",{for:"join-distro"},"Distro",-1)),b(N,{id:"join-distro",modelValue:h.value,"onUpdate:modelValue":a[1]||(a[1]=o=>h.value=o),label:"Distro",options:["ubuntu","debian","fedora","suse","arch","alpine"].map(o=>({value:o,label:o}))},null,8,["modelValue","options"])])):r("",!0),b(O,{variant:"primary",pending:p.value,disabled:p.value,onClick:z},{default:R(()=>[...a[14]||(a[14]=[f(" Reveal ",-1)])]),_:1},8,["pending","disabled"]),e("span",ye,n((j=C.find(o=>o.value===m.value))==null?void 0:j.blurb),1)]),v.value?(t(),y(A,{key:0,text:v.value,title:"Nothing was revealed."},null,8,["text"])):r("",!0),u.value?(t(),s("div",ke,[a[15]||(a[15]=e("b",null,"Secret, shown once — whoever has it can add a node that runs any task here.",-1)),a[16]||(a[16]=e("p",null,[f(" If it leaks, rotate it on the manager: "),e("code",null,"docker swarm join-token --rotate worker")],-1)),e("pre",we,n(u.value),1),e("div",_e,[e("button",{onClick:B},"Copy"),e("button",{onClick:a[2]||(a[2]=o=>u.value="")},"Hide")])])):r("",!0)])):r("",!0)])}}});export{De as default};
|
|
2
|
+
//# sourceMappingURL=SwarmView-BHtmW4QF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SwarmView-BHtmW4QF.js","sources":["../../src/views/SwarmView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The swarm this host manages: its nodes, and what a new worker needs to join.\n *\n * Read straight from docker on every refresh — there is no cached node list anywhere (a table of\n * what is running is exactly what AGENTS.md invariant 10 refuses). Three states are kept apart:\n * docker did not answer (nothing is known), this daemon is not a manager (previews run with\n * compose), and active (the node table).\n *\n * THE JOIN TOKEN IS A SECRET. Whoever holds it can add a node that runs any task on the cluster.\n * It is fetched on a click and never by the poll, shown once, held in component state only, and\n * cleared the moment this page is left.\n */\nimport { onBeforeUnmount, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport type { SwarmInfo } from '../api/types';\nimport { usePolling } from '../composables/usePolling';\nimport { sentence } from '../composables/useFormat';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport RefreshButton from '../components/RefreshButton.vue';\nimport SelectMenu from '../components/SelectMenu.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\n\nconst info = ref<SwarmInfo | null>(null);\nconst error = ref('');\nconst loaded = ref(false);\n\nasync function load(): Promise<void> {\n const r = await api.get<SwarmInfo>('/api/swarm');\n loaded.value = true;\n if (!r.ok) {\n error.value = problem(r, 'read the swarm');\n return;\n }\n error.value = '';\n info.value = {\n ...r.body,\n nodes: r.body.nodes ?? [],\n ports: r.body.ports ?? [],\n };\n}\nusePolling(load, 10_000);\n\n// ── joining ─────────────────────────────────────────────────────────────────────────────────────\ntype Format = 'token' | 'command' | 'script' | 'cloud-config';\nconst format = ref<Format>('command');\nconst distro = ref('ubuntu');\nconst revealed = ref('');\nconst revealing = ref(false);\nconst joinError = ref('');\n\nconst FORMATS: Array<{ value: Format; label: string; blurb: string }> = [\n { value: 'command', label: 'docker swarm join …', blurb: 'For a machine already running Docker.' },\n { value: 'script', label: 'Shell script', blurb: 'Installs Docker, then joins.' },\n { value: 'cloud-config', label: 'cloud-config', blurb: 'User-data for a fresh machine.' },\n { value: 'token', label: 'Token only', blurb: 'For your own tooling.' },\n];\n\nasync function reveal(): Promise<void> {\n revealing.value = true;\n joinError.value = '';\n revealed.value = '';\n const q = `format=${format.value}${format.value === 'cloud-config' ? `&distro=${encodeURIComponent(distro.value)}` : ''}`;\n const r = await api.getText(`/api/swarm/join?${q}`);\n revealing.value = false;\n if (r.status === 403) {\n joinError.value = 'The join token is admin-only.';\n return;\n }\n if (r.status === 409) {\n joinError.value = 'This host is not a swarm manager.';\n return;\n }\n if (!r.ok) {\n joinError.value = r.text || `HTTP ${r.status}`;\n return;\n }\n revealed.value = r.text;\n}\n\nasync function copy(): Promise<void> {\n try {\n await navigator.clipboard.writeText(revealed.value);\n toast('ok', 'Copied.');\n } catch {\n toast('error', 'Copy failed — select it and copy by hand.');\n }\n}\n\n// Leaving the page forgets the secret; coming back means asking for it again.\nonBeforeUnmount(() => {\n revealed.value = '';\n});\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <!-- How swarm mode works, what a worker can and cannot do, and where a worker's volumes\n live is docs/usage.md's job; this page shows the cluster as it is right now. -->\n <h1>Swarm</h1>\n <div class=\"sub\">The nodes previews run on</div>\n </div>\n <span class=\"grow\" />\n <RefreshButton :run=\"load\" />\n </div>\n\n <ErrorNote v-if=\"error\" :text=\"error\" title=\"Could not read the swarm.\" />\n\n <SkeletonList v-if=\"!loaded && !error\" :rows=\"4\" />\n\n <!-- ============================ the cluster ============================ -->\n <section v-else-if=\"info\" class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Nodes</h2>\n <span class=\"grow\" />\n <span v-if=\"info.active && info.managerAddr\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n manager <span class=\"mono\">{{ info.managerAddr }}</span>\n </span>\n </div>\n\n <!-- Three states, never folded: unknown is not \"no nodes\", and not-a-manager is not an error. -->\n <div v-if=\"!info.reachable\" class=\"banner warn\">\n <b>Docker did not answer — this is not the same as no swarm.</b>\n </div>\n <div v-else-if=\"!info.active\" class=\"banner plain\">\n <b>Not a swarm manager.</b>\n <p>{{ info.note }}</p>\n </div>\n <template v-else>\n <div v-if=\"info.error\" class=\"banner warn\">\n <b>Docker reported a problem.</b>\n <p class=\"mono\">{{ info.error }}</p>\n </div>\n <table v-if=\"info.nodes.length\" role=\"table\" class=\"cards\">\n <thead role=\"rowgroup\">\n <tr role=\"row\">\n <th role=\"columnheader\">Hostname</th>\n <th role=\"columnheader\">Role</th>\n <th role=\"columnheader\">Status</th>\n <th role=\"columnheader\">Availability</th>\n <th role=\"columnheader\">Engine</th>\n <th role=\"columnheader\">Id</th>\n </tr>\n </thead>\n <tbody role=\"rowgroup\" class=\"stagger\">\n <tr v-for=\"(n, i) in info.nodes\" :key=\"n.id\" role=\"row\" :style=\"{ '--i': i }\">\n <td role=\"cell\" class=\"name\" data-label=\"hostname\">\n {{ n.hostname || '—' }}\n <span v-if=\"n.self\" class=\"badge info\" title=\"the node this control plane runs on\">This node</span>\n </td>\n <td role=\"cell\" data-label=\"role\">\n <span class=\"badge\" :class=\"n.role === 'manager' ? 'isolated' : ''\">{{ sentence(n.role) }}</span>\n <span v-if=\"n.managerStatus\" class=\"mute\" style=\"font-size: var(--t-sm)\"> {{ n.managerStatus }}</span>\n </td>\n <td role=\"cell\" data-label=\"status\">\n <span :class=\"n.status === 'ready' ? 's-ok' : 's-failed'\">{{ sentence(n.status) }}</span>\n </td>\n <td role=\"cell\" data-label=\"availability\">{{ sentence(n.availability || 'unknown') }}</td>\n <td role=\"cell\" data-label=\"engine\" class=\"mono\">{{ n.engineVersion || '—' }}</td>\n <td role=\"cell\" data-label=\"id\" class=\"mono mute\">{{ n.id.slice(0, 12) }}</td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"mute\">Docker lists no nodes.</p>\n </template>\n </section>\n\n <!-- ============================ add a worker ============================ -->\n <section v-if=\"info?.active\" class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Add a worker</h2>\n </div>\n\n <p class=\"dim\">Open these between the new machine and every other node.</p>\n <ul class=\"kvlist\" style=\"margin-bottom: var(--s4)\">\n <li v-for=\"p in info.ports\" :key=\"p.port\">\n <span class=\"k mono\">{{ p.port }}</span>\n <span class=\"v mute\">{{ p.why }}</span>\n </li>\n </ul>\n\n <div class=\"row\" style=\"flex-wrap: wrap; gap: var(--s3)\">\n <div class=\"field inline\">\n <label for=\"join-format\">Format</label>\n <SelectMenu\n id=\"join-format\"\n :model-value=\"format\"\n label=\"Join format\"\n :options=\"FORMATS.map((f) => ({ value: f.value, label: f.label }))\"\n @update:model-value=\"(v) => (format = v as Format)\"\n />\n </div>\n <div v-if=\"format === 'cloud-config'\" class=\"field inline\">\n <label for=\"join-distro\">Distro</label>\n <SelectMenu\n id=\"join-distro\"\n v-model=\"distro\"\n label=\"Distro\"\n :options=\"['ubuntu', 'debian', 'fedora', 'suse', 'arch', 'alpine'].map((d) => ({ value: d, label: d }))\"\n />\n </div>\n <ActionButton variant=\"primary\" :pending=\"revealing\" :disabled=\"revealing\" @click=\"reveal\">\n Reveal\n </ActionButton>\n <span class=\"mute\" style=\"font-size: var(--t-sm)\">{{ FORMATS.find((f) => f.value === format)?.blurb }}</span>\n </div>\n\n <ErrorNote v-if=\"joinError\" :text=\"joinError\" title=\"Nothing was revealed.\" />\n\n <div v-if=\"revealed\" class=\"banner leaked\" style=\"margin-top: var(--s3)\">\n <b>Secret, shown once — whoever has it can add a node that runs any task here.</b>\n <p>\n If it leaks, rotate it on the manager:\n <code>docker swarm join-token --rotate worker</code>\n </p>\n <pre class=\"code\" style=\"white-space: pre-wrap; word-break: break-all; max-height: 50vh; overflow: auto\">{{ revealed }}</pre>\n <div class=\"row\">\n <button @click=\"copy\">Copy</button>\n <button @click=\"revealed = ''\">Hide</button>\n </div>\n </div>\n </section>\n </div>\n</template>\n"],"names":["info","ref","error","loaded","load","r","api","problem","usePolling","format","distro","revealed","revealing","joinError","FORMATS","reveal","q","copy","toast","onBeforeUnmount","_createElementBlock","_createElementVNode","_hoisted_1","_createVNode","RefreshButton","_createBlock","ErrorNote","SkeletonList","_openBlock","_hoisted_2","_hoisted_3","_cache","_hoisted_4","_hoisted_5","_toDisplayString","_Fragment","_hoisted_8","_hoisted_9","_hoisted_10","_hoisted_11","n","i","_hoisted_12","_hoisted_13","_hoisted_14","_normalizeClass","_unref","sentence","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_7","_hoisted_6","_a","_hoisted_21","_hoisted_22","_renderList","p","_hoisted_23","_hoisted_24","_hoisted_25","_hoisted_26","SelectMenu","f","v","_hoisted_27","$event","d","ActionButton","_hoisted_28","_b","_hoisted_29","_hoisted_30","_hoisted_31"],"mappings":"u2DAyBA,MAAMA,EAAOC,EAAsB,IAAI,EACjCC,EAAQD,EAAI,EAAE,EACdE,EAASF,EAAI,EAAK,EAExB,eAAeG,GAAsB,CACnC,MAAMC,EAAI,MAAMC,EAAI,IAAe,YAAY,EAE/C,GADAH,EAAO,MAAQ,GACX,CAACE,EAAE,GAAI,CACTH,EAAM,MAAQK,EAAQF,EAAG,gBAAgB,EACzC,MACF,CACAH,EAAM,MAAQ,GACdF,EAAK,MAAQ,CACX,GAAGK,EAAE,KACL,MAAOA,EAAE,KAAK,OAAS,CAAA,EACvB,MAAOA,EAAE,KAAK,OAAS,CAAA,CAAC,CAE5B,CACAG,EAAWJ,EAAM,GAAM,EAIvB,MAAMK,EAASR,EAAY,SAAS,EAC9BS,EAAST,EAAI,QAAQ,EACrBU,EAAWV,EAAI,EAAE,EACjBW,EAAYX,EAAI,EAAK,EACrBY,EAAYZ,EAAI,EAAE,EAElBa,EAAkE,CACtE,CAAE,MAAO,UAAW,MAAO,sBAAuB,MAAO,uCAAA,EACzD,CAAE,MAAO,SAAU,MAAO,eAAgB,MAAO,8BAAA,EACjD,CAAE,MAAO,eAAgB,MAAO,eAAgB,MAAO,gCAAA,EACvD,CAAE,MAAO,QAAS,MAAO,aAAc,MAAO,uBAAA,CAAwB,EAGxE,eAAeC,GAAwB,CACrCH,EAAU,MAAQ,GAClBC,EAAU,MAAQ,GAClBF,EAAS,MAAQ,GACjB,MAAMK,EAAI,UAAUP,EAAO,KAAK,GAAGA,EAAO,QAAU,eAAiB,WAAW,mBAAmBC,EAAO,KAAK,CAAC,GAAK,EAAE,GACjHL,EAAI,MAAMC,EAAI,QAAQ,mBAAmBU,CAAC,EAAE,EAElD,GADAJ,EAAU,MAAQ,GACdP,EAAE,SAAW,IAAK,CACpBQ,EAAU,MAAQ,gCAClB,MACF,CACA,GAAIR,EAAE,SAAW,IAAK,CACpBQ,EAAU,MAAQ,oCAClB,MACF,CACA,GAAI,CAACR,EAAE,GAAI,CACTQ,EAAU,MAAQR,EAAE,MAAQ,QAAQA,EAAE,MAAM,GAC5C,MACF,CACAM,EAAS,MAAQN,EAAE,IACrB,CAEA,eAAeY,GAAsB,CACnC,GAAI,CACF,MAAM,UAAU,UAAU,UAAUN,EAAS,KAAK,EAClDO,EAAM,KAAM,SAAS,CACvB,MAAQ,CACNA,EAAM,QAAS,2CAA2C,CAC5D,CACF,CAGA,OAAAC,EAAgB,IAAM,CACpBR,EAAS,MAAQ,EACnB,CAAC,6BAICS,EAgIM,MAAA,KAAA,CA/HJC,EASM,MATNC,EASM,aARJD,EAKM,MAAA,KAAA,CAFJA,EAAc,UAAV,OAAK,EACTA,EAAgD,MAAA,CAA3C,MAAM,KAAA,EAAM,2BAAyB,CAAA,mBAE5CA,EAAqB,OAAA,CAAf,MAAM,MAAA,EAAM,KAAA,EAAA,GAClBE,EAA6BC,EAAA,CAAb,IAAKpB,EAAI,CAAA,GAGVF,EAAA,WAAjBuB,EAA0EC,EAAA,OAAjD,KAAMxB,EAAA,MAAO,MAAM,2BAAA,6BAEvB,CAAAC,EAAA,QAAWD,EAAA,WAAhCuB,EAAmDE,EAAA,OAAX,KAAM,CAAA,IAG1B3B,EAAA,OAApB4B,IAAAR,EAsDU,UAtDVS,EAsDU,CArDRR,EAMM,MANNS,EAMM,CALJC,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAV,EAA8B,KAAA,CAA1B,MAAM,SAAA,EAAU,QAAK,EAAA,eACzBA,EAAqB,OAAA,CAAf,MAAM,MAAA,EAAM,KAAA,EAAA,GACNrB,EAAA,MAAK,QAAUA,EAAA,MAAK,aAAhC4B,IAAAR,EAEO,OAFPY,EAEO,eAFkF,YAC/E,EAAA,GAAAX,EAAgD,OAAhDY,EAAgDC,EAA1BlC,EAAA,MAAK,WAAW,EAAA,CAAA,CAAA,eAKtCA,EAAA,MAAK,UAGAA,EAAA,MAAK,YAItBoB,EAoCWe,EAAA,CAAA,IAAA,GAAA,CAnCEnC,EAAA,MAAK,OAAhB4B,IAAAR,EAGM,MAHNgB,EAGM,CAFJL,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAV,EAAiC,SAA9B,6BAA0B,EAAA,GAC7BA,EAAoC,IAApCgB,EAAoCH,EAAjBlC,EAAA,MAAK,KAAK,EAAA,CAAA,CAAA,aAElBA,EAAA,MAAK,MAAM,QAAxB4B,IAAAR,EA6BQ,QA7BRkB,GA6BQ,eA5BNjB,EASQ,QAAA,CATD,KAAK,YAAU,CACpBA,EAOK,KAAA,CAPD,KAAK,OAAK,CACZA,EAAqC,KAAA,CAAjC,KAAK,cAAA,EAAe,UAAQ,EAChCA,EAAiC,KAAA,CAA7B,KAAK,cAAA,EAAe,MAAI,EAC5BA,EAAmC,KAAA,CAA/B,KAAK,cAAA,EAAe,QAAM,EAC9BA,EAAyC,KAAA,CAArC,KAAK,cAAA,EAAe,cAAY,EACpCA,EAAmC,KAAA,CAA/B,KAAK,cAAA,EAAe,QAAM,EAC9BA,EAA+B,KAAA,CAA3B,KAAK,cAAA,EAAe,IAAE,CAAA,SAG9BA,EAiBQ,QAjBRkB,GAiBQ,EAhBNX,EAAA,EAAA,EAAAR,EAeKe,SAfgBnC,EAAA,MAAK,MAAK,CAAnBwC,EAAGC,SAAfrB,EAeK,KAAA,CAf6B,IAAKoB,EAAE,GAAI,KAAK,MAAO,eAAgBC,EAAC,CAAA,GACxEpB,EAGK,KAHLqB,GAGK,KAFAF,EAAE,UAAQ,GAAA,EAAU,IACvB,CAAA,EAAYA,EAAE,UAAdpB,EAAmG,OAAnGuB,GAAmF,WAAS,cAE9FtB,EAGK,KAHLuB,GAGK,CAFHvB,EAAiG,OAAA,CAA3F,MAAKwB,EAAA,CAAC,QAAgBL,EAAE,OAAI,UAAA,WAAA,EAAA,CAAA,CAAA,IAAqCM,EAAAC,CAAA,EAASP,EAAE,IAAI,CAAA,EAAA,CAAA,EAC1EA,EAAE,eAAdZ,EAAA,EAAAR,EAAsG,OAAtG4B,GAAsGd,EAAzBM,EAAE,aAAa,EAAA,CAAA,cAE9FnB,EAEK,KAFL4B,GAEK,CADH5B,EAAyF,OAAA,CAAlF,MAAKwB,EAAEL,EAAE,SAAM,QAAA,OAAA,UAAA,CAAA,IAAuCM,EAAAC,CAAA,EAASP,EAAE,MAAM,CAAA,EAAA,CAAA,CAAA,GAEhFnB,EAA0F,KAA1F6B,GAA0FhB,EAA7CY,KAASN,EAAE,cAAY,SAAA,CAAA,EAAA,CAAA,EACpEnB,EAAkF,KAAlF8B,GAAkFjB,EAA9BM,EAAE,eAAa,GAAA,EAAA,CAAA,EACnEnB,EAA8E,KAA9E+B,GAA8ElB,EAAzBM,EAAE,GAAG,MAAK,EAAA,EAAA,CAAA,EAAA,CAAA,CAAA,uBAIrEpB,EAAiD,IAAjDiC,GAAuB,wBAAsB,EAAA,QAvC/CzB,IAAAR,EAGM,MAHNkC,EAGM,CAFJvB,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAV,EAA2B,SAAxB,uBAAoB,EAAA,GACvBA,EAAsB,IAAA,KAAAa,EAAhBlC,EAAA,MAAK,IAAI,EAAA,CAAA,CAAA,KALjB4B,EAAA,EAAAR,EAEM,MAFNmC,EAEM,CAAA,GAAAxB,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAA,CADJV,EAAgE,SAA7D,4DAAyD,EAAA,CAAA,mBA8CjDmC,EAAAxD,EAAA,QAAA,MAAAwD,EAAM,QAArB5B,IAAAR,EAqDU,UArDVqC,GAqDU,eApDRpC,EAEM,MAAA,CAFD,MAAM,SAAO,CAChBA,EAAqC,KAAA,CAAjC,MAAM,SAAA,EAAU,cAAY,CAAA,OAGlCU,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAV,EAA2E,IAAA,CAAxE,MAAM,KAAA,EAAM,2DAAwD,EAAA,GACvEA,EAKK,KALLqC,GAKK,EAJH9B,EAAA,EAAA,EAAAR,EAGKe,EAAA,KAAAwB,EAHW3D,EAAA,MAAK,MAAV4D,QAAXxC,EAGK,KAAA,CAHwB,IAAKwC,EAAE,IAAA,GAClCvC,EAAwC,OAAxCwC,GAAwC3B,EAAhB0B,EAAE,IAAI,EAAA,CAAA,EAC9BvC,EAAuC,OAAvCyC,GAAuC5B,EAAf0B,EAAE,GAAG,EAAA,CAAA,CAAA,aAIjCvC,EAwBM,MAxBN0C,GAwBM,CAvBJ1C,EASM,MATN2C,GASM,CARJjC,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAV,EAAuC,QAAA,CAAhC,IAAI,aAAA,EAAc,SAAM,EAAA,GAC/BE,EAME0C,EAAA,CALA,GAAG,cACF,cAAaxD,EAAA,MACd,MAAM,cACL,QAASK,EAAQ,IAAKoD,IAAC,CAAA,MAAeA,EAAE,MAAK,MAASA,EAAE,KAAA,EAAK,EAC7D,sBAAkBnC,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAGoC,GAAO1D,EAAA,MAAS0D,EAAA,sCAG/B1D,EAAA,QAAM,gBAAjBmB,IAAAR,EAQM,MARNgD,GAQM,CAPJrC,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAV,EAAuC,QAAA,CAAhC,IAAI,aAAA,EAAc,SAAM,EAAA,GAC/BE,EAKE0C,EAAA,CAJA,GAAG,yBACMvD,EAAA,2CAAAA,EAAM,MAAA2D,GACf,MAAM,SACL,4DAAkE,IAAKC,IAAC,CAAA,MAAeA,QAAUA,GAAC,CAAA,+CAGvG/C,EAEegD,EAAA,CAFD,QAAQ,UAAW,QAAS3D,EAAA,MAAY,SAAUA,EAAA,MAAY,QAAOG,CAAA,aAAQ,IAE3F,CAAA,GAAAgB,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAA,GAF2F,WAE3F,EAAA,CAAA,oCACAV,EAA6G,OAA7GmD,GAA6GtC,GAAxDuC,EAAA3D,EAAQ,KAAMoD,GAAMA,EAAE,QAAUzD,EAAA,KAAM,IAAtC,YAAAgE,EAAyC,KAAK,EAAA,CAAA,CAAA,GAGpF5D,EAAA,WAAjBY,EAA8EC,EAAA,OAAjD,KAAMb,EAAA,MAAW,MAAM,uBAAA,6BAEzCF,EAAA,OAAXiB,EAAA,EAAAR,EAWM,MAXNsD,GAWM,CAVJ3C,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAV,EAAkF,SAA/E,8EAA2E,EAAA,iBAC9EA,EAGI,IAAA,KAAA,GAHD,0CAED,EAAAA,EAAoD,YAA9C,yCAAuC,CAAA,OAE/CA,EAA6H,MAA7HsD,GAA6HzC,EAAjBvB,EAAA,KAAQ,EAAA,CAAA,EACpHU,EAGM,MAHNuD,GAGM,CAFJvD,EAAmC,SAAA,CAA1B,QAAOJ,CAAA,EAAM,MAAI,EAC1BI,EAA4C,SAAA,CAAnC,uBAAOV,EAAA,MAAQ,GAAA,EAAO,MAAI,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.terminal-host[data-v-
|
|
1
|
+
.terminal-host[data-v-fb326a53]{min-height:26rem;border:1px solid var(--line);border-radius:var(--r3);background:#0b0e14;padding:var(--s2);overflow:hidden}.terminal-host[data-state=idle][data-v-fb326a53]{display:none}
|
|
@@ -0,0 +1,3 @@
|
|
|
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 F,N as O,Q as H,c as p,a as l,n as T,z as W,f as g,e as P,t as U,i as c,j as V,p as j,C as M,a8 as E,m as Q,a9 as S,o as d}from"./index-D_Ks6aQi.js";import{_ as Y}from"./ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js";import{_ as A}from"./SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js";import{_ as G}from"./_plugin-vue_export-helper-DlAUqK2U.js";const J={class:"stack"},K={class:"row wrap",style:{gap:"var(--s3)"}},X={class:"field inline"},Z={class:"field inline"},ee=["disabled"],ne={key:2,class:"badge ok"},oe={key:1,class:"hint"},te={key:2,class:"hint"},ae=["data-state"],le=F({__name:"TerminalTab",setup(se){const w=Q(),k=V(()=>String(w.params.id)),B=["sh","bash","ash","zsh","fish"],R=c(null),m=c(""),r=c(""),_=c("sh"),n=c("idle"),f=c(""),v=c(null),h=S(null),I=S(null),b=S(null);let s=null;const D=V(()=>y.value.map(o=>({value:o.name,label:o.name,hint:o.service??void 0}))),y=V(()=>{var o;return((o=R.value)==null?void 0:o.containers.filter(e=>e.state==="running"))??[]});async function N(){var u;const o=await j.get(`/api/deployments/${encodeURIComponent(k.value)}/runtime`);if(!o.ok){m.value=M(o,"list containers");return}if(m.value="",R.value=o.body,r.value)return;const e=typeof w.query.container=="string"?w.query.container:"",t=y.value.find(C=>C.name===e);r.value=(t==null?void 0:t.name)??((u=y.value[0])==null?void 0:u.name)??""}N();async function z(){var L;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(a=>a.x),__vite__mapDeps([0,1])),E(()=>import("./addon-fit-CevVMrUA.js").then(a=>a.a),__vite__mapDeps([2,1]))]);await E(()=>Promise.resolve({}),__vite__mapDeps([3])),(L=h.value)==null||L.dispose();const t=new o({convertEol:!0,fontSize:13,fontFamily:"var(--mono, ui-monospace, monospace)",cursorBlink:!0,theme:{background:"#0b0e14"}}),u=new e;t.loadAddon(u),v.value&&(v.value.innerHTML="",t.open(v.value),u.fit()),h.value=t,I.value=u,s==null||s.disconnect(),s=new ResizeObserver(()=>{try{u.fit()}catch{}}),v.value&&s.observe(v.value);const C=location.protocol==="https:"?"wss":"ws",q=new URLSearchParams({container:r.value,shell:_.value}),i=new WebSocket(`${C}://${location.host}/api/deployments/${encodeURIComponent(k.value)}/terminal?${q}`);i.binaryType="arraybuffer",b.value=i,i.onopen=()=>{n.value="open",t.focus()},i.onmessage=a=>t.write(typeof a.data=="string"?a.data:new Uint8Array(a.data)),i.onclose=a=>{n.value="closed",f.value=a.reason||(a.code===1006?"connection dropped":`code ${a.code}`)},i.onerror=()=>{n.value==="connecting"&&(f.value="could not connect")},t.onData(a=>{i.readyState===WebSocket.OPEN&&i.send(a)})}function $(){var o;(o=b.value)==null||o.close(1e3,"closed by the operator")}function x(){var o,e;s==null||s.disconnect(),s=null,(o=b.value)==null||o.close(),b.value=null,(e=h.value)==null||e.dispose(),h.value=null}return O([r,_],()=>{(n.value==="open"||n.value==="connecting")&&$()}),O(k,()=>{x(),n.value="idle",N()}),H(x),(o,e)=>(d(),p("section",J,[e[6]||(e[6]=l("div",{class:"banner warn"},[l("b",null,"No TTY"),T(" — no prompt, no "),l("code",null,"Ctrl-C"),T(", no full-screen programs. ")],-1)),m.value?(d(),W(Y,{key:0,text:m.value,title:"Could not list containers."},null,8,["text"])):g("",!0),l("div",K,[l("div",X,[e[2]||(e[2]=l("label",{for:"term-container"},"Container",-1)),P(A,{id:"term-container",modelValue:r.value,"onUpdate:modelValue":e[0]||(e[0]=t=>r.value=t),label:"Container",disabled:n.value==="open"||n.value==="connecting",options:D.value},null,8,["modelValue","disabled","options"])]),l("div",Z,[e[3]||(e[3]=l("label",{for:"term-shell"},"Shell",-1)),P(A,{id:"term-shell",modelValue:_.value,"onUpdate:modelValue":e[1]||(e[1]=t=>_.value=t),label:"Shell",disabled:n.value==="open"||n.value==="connecting",options:B.map(t=>({value:t,label:t}))},null,8,["modelValue","disabled","options"])]),n.value!=="open"?(d(),p("button",{key:0,class:"primary",disabled:!r.value||n.value==="connecting",onClick:z},U(n.value==="connecting"?"Connecting…":"Open shell"),9,ee)):(d(),p("button",{key:1,onClick:$},"Close shell")),e[5]||(e[5]=l("span",{class:"grow"},null,-1)),n.value==="open"?(d(),p("span",ne,[...e[4]||(e[4]=[l("span",{class:"dot pulse"},null,-1),T("Connected",-1)])])):g("",!0)]),!y.value.length&&!m.value?(d(),p("p",oe,"Nothing is running.")):g("",!0),n.value==="closed"&&f.value?(d(),p("p",te,"Session ended — "+U(f.value)+".",1)):g("",!0),l("div",{ref_key:"host",ref:v,class:"terminal-host","data-state":n.value},null,8,ae),e[7]||(e[7]=l("p",{class:"hint"},"Every session is recorded — who, which container, when.",-1))]))}}),de=G(le,[["__scopeId","data-v-fb326a53"]]);export{de as default};
|
|
3
|
+
//# sourceMappingURL=TerminalTab-DF8cSjcg.js.map
|
|
@@ -0,0 +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","wanted","match","connect","Terminal","FitAddon","__vitePreload","n","f","proto","qs","ws","e","d","disconnect","teardown","_b","watch","onBeforeUnmount","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_createBlock","ErrorNote","_hoisted_2","_hoisted_3","_cache","_createVNode","SelectMenu","$event","_hoisted_4","s","_hoisted_5","_hoisted_6","_hoisted_7","_hoisted_8","_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 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 containers');\n return;\n }\n loadError.value = '';\n rt.value = r.body;\n if (container.value) return;\n /*\n * `?container=` is how the Containers table jumps straight to a shell here. Honoured only when it\n * names a container this deployment actually owns and is running — an unknown name falls back to\n * the first, because the alternative is a picker sitting on a container that does not exist and a\n * Connect button that 404s. (The server checks the name again on upgrade; this is convenience.)\n */\n const wanted = typeof route.query.container === 'string' ? route.query.container : '';\n const match = running.value.find((c) => c.name === wanted);\n container.value = match?.name ?? 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(--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 = e.reason || (e.code === 1006 ? 'connection dropped' : `code ${e.code}`);\n };\n ws.onerror = () => {\n if (state.value === 'connecting') closeReason.value = 'could not connect';\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 <!-- No pty: without this line the missing prompt and dead Ctrl-C read as a broken terminal. -->\n <div class=\"banner warn\">\n <b>No TTY</b> — no prompt, no <code>Ctrl-C</code>, no full-screen programs.\n </div>\n\n <ErrorNote v-if=\"loadError\" :text=\"loadError\" title=\"Could not list containers.\" />\n\n <div class=\"row wrap\" style=\"gap: var(--s3)\">\n <div class=\"field inline\">\n <label for=\"term-container\">Container</label>\n <SelectMenu id=\"term-container\" v-model=\"container\" label=\"Container\" :disabled=\"state === 'open' || state === 'connecting'\" :options=\"containerOptions\" />\n </div>\n <div class=\"field inline\">\n <label for=\"term-shell\">Shell</label>\n <SelectMenu id=\"term-shell\" v-model=\"shell\" label=\"Shell\" :disabled=\"state === 'open' || state === 'connecting'\" :options=\"SHELLS.map((s) => ({ value: s, label: s }))\" />\n </div>\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\">Nothing is running.</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 <!-- Audit interlock: the operator is told before typing that the session is on the record. -->\n <p class=\"hint\">Every session is recorded — who, which container, when.</p>\n </section>\n</template>\n\n<style scoped>\n.terminal-host {\n min-height: 26rem;\n border: 1px solid var(--line);\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-DF8cSjcg.js"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.stack-form[data-v-9e649743]{margin-top:var(--s5);padding-top:var(--s4);border-top:1px solid var(--line-soft);display:flex;flex-direction:column;gap:var(--s3)}.stack-form h3[data-v-9e649743]{font-size:var(--t-sm);color:var(--fg-dim)}td.right[data-v-9e649743]{text-align:end}.two-up>.field[data-v-9e649743]{flex:1 1 200px}td.right button[data-v-9e649743]{margin-inline-start:var(--s2)}.role-pick[data-v-9e649743]{display:inline-block}.role-pick[data-v-9e649743] .selm-trigger{width:auto;min-width:150px}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{d as oe,ak as ae,M as O,c as n,a as s,e as d,z as $,f as k,t as u,F as B,r as L,u as l,af as V,w as I,g as D,v as T,b as h,h as _,a0 as te,i as m,j as M,p as w,C as y,o as t,n as x,a5 as le,a1 as ne,a3 as re,a4 as de,a2 as ie,a6 as C,s as ue}from"./index-D_Ks6aQi.js";import{A as F}from"./ActionButton-OewKsFrK.js";import{E as Y}from"./EquivalentCommand-D-mkbReY.js";import{_ as ce}from"./ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js";import{_ as z}from"./RelativeTime.vue_vue_type_script_setup_true_lang-BxIqRu_b.js";import{_ as H}from"./SkeletonList.vue_vue_type_script_setup_true_lang-D4pkKZAr.js";import{_ as pe}from"./RefreshButton.vue_vue_type_script_setup_true_lang-CQvD4Out.js";import{_ as W}from"./SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js";import{_ as me}from"./_plugin-vue_export-helper-DlAUqK2U.js";const ve={class:"page-head"},he={key:1,class:"banner ok"},we={class:"code"},fe={class:"row"},ke={class:"grid-2"},_e={class:"panel"},ye={class:"phead"},be={class:"mute",style:{"font-size":"var(--t-xs)"}},ge={key:1,role:"table",class:"cards"},$e={role:"rowgroup"},Ce={role:"cell",class:"name","data-label":"user"},Ae={key:0,class:"badge info"},Ue={role:"cell","data-label":"role"},Re=["title"],Ve={key:1},De={role:"cell",class:"dim nowrap","data-label":"added"},Te={role:"cell",class:"right nowrap","data-label":""},xe=["onClick"],Ne={key:2,class:"hint"},Se={class:"row wrap two-up"},Pe={class:"field grow"},Oe={class:"field grow"},Ee={class:"field"},Be={class:"row"},Le=["disabled"],Ie={key:4,class:"hint"},Me={class:"panel"},Fe={key:1,role:"table",class:"cards"},Ye={role:"rowgroup"},ze={role:"cell",class:"name","data-label":"name"},He={role:"cell",class:"dim nowrap","data-label":"last used"},We={key:1,class:"mute"},je={role:"cell",class:"right nowrap","data-label":""},qe={key:2,class:"hint"},Ge={class:"field"},Je={class:"row"},Ke=["disabled"],Qe={class:"hint"},Xe={class:"field"},Ze={class:"row"},es=["disabled"],j="The only admin — promote another account first.",ss=oe({__name:"UsersView",setup(os){const A=m([]),N=m([]),S=m(!1),i=m(""),r=m({username:"",password:"",role:"viewer"}),f=m(""),c=m(null),b=m(""),g=m(null),q=M(()=>r.value.username.trim().length>0&&r.value.password.length>=8),G={viewer:"reads everything",developer:"deploys and tears down",maintainer:"host configuration",admin:"accounts and sign-on"},E=ae.map(a=>({value:a,label:O(a),hint:G[a]})),J=M(()=>A.value.filter(a=>a.role==="admin").length);function U(a){return a.role==="admin"&&J.value===1}async function v(){const[a,e]=await Promise.all([w.get("/api/users"),w.get("/api/tokens")]);if(S.value=!0,!a.ok){i.value=y(a,"load the accounts");return}i.value="",A.value=a.body.users??[],N.value=e.ok?e.body.tokens??[]:[]}v();async function K(){const a=await w.post("/api/users",{username:r.value.username.trim(),password:r.value.password,role:r.value.role});if(!a.ok){i.value=y(a,"create this account");return}C("ok",`Added ${a.body.user.username}.`),r.value={username:"",password:"",role:"viewer"},v()}async function Q(a){const e=await w.del(`/api/users/${a.id}`);if(!e.ok){i.value=y(e,`delete ${a.username}`);return}C("ok",`Deleted ${a.username}.`),v()}async function X(a,e){var p;if(e===a.role)return;const o=await w.patch(`/api/users/${a.id}`,{role:e});if(!o.ok){i.value=y(o,`change the role for ${a.username}`);return}i.value="",a.id===((p=h.user)==null?void 0:p.id)?(await ue(),C("ok",`You are now a ${e}.`)):C("ok",`${a.username} is now a ${e}.`),v()}async function Z(){var o;const a=c.value;if(!a)return;const e=await w.put(`/api/users/${a.id}/password`,{password:b.value});if(!e.ok){i.value=y(e,`change the password for ${a.username}`);return}c.value=null,b.value="",C("ok",a.id===((o=h.user)==null?void 0:o.id)?"Changed — sign in again.":"Password changed."),v()}async function ee(){const a=await w.post("/api/tokens",{name:f.value.trim()});if(!a.ok){i.value=y(a,"create this token");return}g.value={what:`Personal token “${f.value.trim()}”`,value:a.body.token,note:"Shown once — the server keeps only a hash. It can do anything your account can."},f.value="",v()}async function se(a){const e=await w.del(`/api/tokens/${a.id}`);if(!e.ok){i.value=y(e,`revoke ${a.name}`);return}C("ok",`Revoked ${a.name}.`),v()}return(a,e)=>(t(),n("div",null,[s("div",ve,[e[7]||(e[7]=s("div",null,[s("h1",null,"Users & access"),s("div",{class:"sub"},"Accounts and personal tokens")],-1)),e[8]||(e[8]=s("span",{class:"grow"},null,-1)),d(pe,{run:v})]),i.value?(t(),$(ce,{key:0,text:i.value,title:"Could not load the accounts."},null,8,["text"])):k("",!0),g.value?(t(),n("div",he,[s("b",null,u(g.value.what)+" — copy it now.",1),s("p",null,u(g.value.note),1),s("pre",we,u(g.value.value),1),s("div",fe,[s("button",{onClick:e[0]||(e[0]=o=>g.value=null)},"Dismiss")])])):k("",!0),s("div",ke,[s("section",_e,[s("div",ye,[e[9]||(e[9]=s("h2",{class:"section"},"Accounts",-1)),e[10]||(e[10]=s("span",{class:"grow"},null,-1)),s("span",be,u(A.value.length),1)]),S.value?A.value.length?(t(),n("table",ge,[e[12]||(e[12]=s("thead",{role:"rowgroup"},[s("tr",{role:"row"},[s("th",{role:"columnheader"},"User"),s("th",{role:"columnheader"},"Role"),s("th",{role:"columnheader"},"Added"),s("th",{role:"columnheader","aria-label":"Actions"})])],-1)),s("tbody",$e,[(t(!0),n(B,null,L(A.value,o=>{var p,R;return t(),n("tr",{key:o.id,role:"row"},[s("td",Ce,[s("b",null,u(o.username),1),o.id===((p=l(h).user)==null?void 0:p.id)?(t(),n("span",Ae,"you")):k("",!0)]),s("td",Ue,[l(V)("admin")?(t(),n("span",{key:0,class:"role-pick",title:U(o)?j:void 0},[d(W,{"model-value":o.role,label:`Role for ${o.username}`,options:l(E),disabled:U(o),placeholder:l(O)(o.role),"onUpdate:modelValue":P=>X(o,P)},null,8,["model-value","label","options","disabled","placeholder","onUpdate:modelValue"])],8,Re)):(t(),n("span",Ve,u(l(O)(o.role)),1))]),s("td",De,[d(z,{at:o.createdAt},null,8,["at"])]),s("td",Te,[o.id===((R=l(h).user)==null?void 0:R.id)||l(V)("admin")?(t(),n("button",{key:0,class:"ghost sm",onClick:P=>{c.value=o,b.value=""}}," Change password ",8,xe)):k("",!0),l(V)("admin")?(t(),$(F,{key:1,class:"danger sm",confirm:`Delete ${o.username}?`,disabled:U(o),title:U(o)?j:void 0,onRun:P=>Q(o)},{default:_(()=>[...e[11]||(e[11]=[x("Delete",-1)])]),_:1},8,["confirm","disabled","title","onRun"])):k("",!0)])])}),128))])])):(t(),n("p",Ne,"No accounts yet.")):(t(),$(H,{key:0,rows:3,tall:""})),l(V)("admin")?(t(),n("form",{key:3,class:"stack-form",onSubmit:I(K,["prevent"])},[e[17]||(e[17]=s("h3",null,"Add an account",-1)),s("div",Se,[s("label",Pe,[e[13]||(e[13]=s("span",null,"Username",-1)),D(s("input",{"onUpdate:modelValue":e[1]||(e[1]=o=>r.value.username=o),type:"text",autocomplete:"off"},null,512),[[T,r.value.username]])]),s("label",Oe,[e[14]||(e[14]=s("span",null,"Password",-1)),D(s("input",{"onUpdate:modelValue":e[2]||(e[2]=o=>r.value.password=o),type:"password",autocomplete:"new-password"},null,512),[[T,r.value.password]])])]),s("div",Ee,[e[15]||(e[15]=s("label",{for:"new-role"},"Role",-1)),d(W,{id:"new-role","model-value":r.value.role,label:"Role for the new account",options:l(E),"onUpdate:modelValue":e[3]||(e[3]=o=>r.value.role=o)},null,8,["model-value","options"])]),e[18]||(e[18]=s("p",{class:"hint"},"At least 8 characters.",-1)),s("div",Be,[s("button",{class:"primary",type:"submit",disabled:!q.value},"Add account",8,Le),e[16]||(e[16]=s("span",{class:"grow"},null,-1))]),d(Y,{what:"this account creation",method:"POST",path:"/api/users",body:{username:r.value.username||"alice",password:"…",role:r.value.role}},null,8,["body"])],32)):(t(),n("p",Ie,"Only an admin can add accounts or change roles."))]),s("section",Me,[e[23]||(e[23]=s("div",{class:"phead"},[s("h2",{class:"section"},"Your API tokens")],-1)),S.value?N.value.length?(t(),n("table",Fe,[e[20]||(e[20]=s("thead",{role:"rowgroup"},[s("tr",{role:"row"},[s("th",{role:"columnheader"},"Name"),s("th",{role:"columnheader"},"Last used"),s("th",{role:"columnheader","aria-label":"Actions"})])],-1)),s("tbody",Ye,[(t(!0),n(B,null,L(N.value,o=>(t(),n("tr",{key:o.id,role:"row"},[s("td",ze,[s("b",null,u(o.name),1)]),s("td",He,[o.lastUsedAt?(t(),$(z,{key:0,at:o.lastUsedAt},null,8,["at"])):(t(),n("span",We,"never"))]),s("td",je,[d(F,{class:"danger sm",confirm:`Revoke ${o.name}?`,onRun:p=>se(o)},{default:_(()=>[...e[19]||(e[19]=[x("Revoke",-1)])]),_:1},8,["confirm","onRun"])])]))),128))])])):(t(),n("p",qe,u(l(h).root?"The host token has no personal tokens.":"No tokens yet — name one below."),1)):(t(),$(H,{key:0,rows:2,tall:""})),l(h).root?k("",!0):(t(),n("form",{key:3,class:"stack-form",onSubmit:I(ee,["prevent"])},[e[22]||(e[22]=s("h3",null,"New token",-1)),s("label",Ge,[e[21]||(e[21]=s("span",null,"Name",-1)),D(s("input",{"onUpdate:modelValue":e[4]||(e[4]=o=>f.value=o),type:"text",placeholder:"ci-deploy",autocomplete:"off"},null,512),[[T,f.value]])]),s("div",Je,[s("button",{class:"primary",type:"submit",disabled:!f.value.trim()},"Create token",8,Ke)]),d(Y,{what:"this token creation",method:"POST",path:"/api/tokens",body:{name:f.value||"ci-deploy"}},null,8,["body"])],32))])]),d(l(te),{open:!!c.value,"onUpdate:open":e[6]||(e[6]=o=>!o&&(c.value=null))},{default:_(()=>[d(l(le),null,{default:_(()=>[d(l(ne),{class:"scrim help-scrim"}),c.value?(t(),$(l(ie),{key:0,class:"sheet","aria-label":"Change password"},{default:_(()=>{var o,p;return[d(l(re),{as:"h3"},{default:_(()=>[x("Change password for "+u(c.value.username),1)]),_:1}),s("p",Qe," Signs "+u(c.value.id===((o=l(h).user)==null?void 0:o.id)?"you":"them")+" out everywhere and revokes "+u(c.value.id===((p=l(h).user)==null?void 0:p.id)?"your":"their")+" tokens. ",1),s("label",Xe,[e[24]||(e[24]=s("span",null,"New password",-1)),D(s("input",{"onUpdate:modelValue":e[5]||(e[5]=R=>b.value=R),type:"password",autocomplete:"new-password"},null,512),[[T,b.value]])]),s("div",Ze,[s("button",{class:"primary",disabled:b.value.length<8,onClick:Z}," Change password ",8,es),d(l(de),{class:"ghost"},{default:_(()=>[...e[25]||(e[25]=[x("Cancel",-1)])]),_:1})])]}),_:1})):k("",!0)]),_:1})]),_:1},8,["open"])]))}}),ps=me(ss,[["__scopeId","data-v-9e649743"]]);export{ps as default};
|
|
2
|
+
//# sourceMappingURL=UsersView-CbPtmnP6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UsersView-CbPtmnP6.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 *\n * ── ROLES ────────────────────────────────────────────────────────────────────────────────────────\n *\n * The roster is ordinary team information, so ANY signed-in account reads this page. Changing it —\n * adding an account, setting a role, deleting one, setting someone ELSE's password — is admin's,\n * and those controls are simply not rendered for anyone else. Your OWN password stays yours at\n * every role, so that one button is there for everybody, on their own row only.\n *\n * Hiding is UX. `can('admin')` decides what to DRAW; the server's permission table decides what is\n * allowed, and a hidden control that is somehow reached still comes back 403 — which lands in\n * `listError` and says which role the route wanted. That path is not a fallback, it is the actual\n * enforcement, so it stays.\n *\n * THE LAST ADMIN is the one rule the server has that the UI can see coming: it refuses to delete or\n * demote the final admin account. A request that can only fail is worse than a control that says\n * why it cannot be pressed, so that row's role picker and Delete arrive already disabled, carrying\n * the same remedy the server would have answered with — promote another account first.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport { ROLES, type Role, type User, type UsersResponse } from '../api/types';\nimport { authState, can, checkAuth } from '../composables/useAuth';\nimport { sentence } from '../composables/useFormat';\nimport {\n DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogClose,\n} from 'reka-ui';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport RelativeTime from '../components/RelativeTime.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\nimport RefreshButton from '../components/RefreshButton.vue';\nimport SelectMenu from '../components/SelectMenu.vue';\n\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: string; password: string; role: Role }>({\n username: '',\n password: '',\n role: 'viewer',\n});\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\n/** One line each, in the picker itself — a role name alone does not say what it costs to grant. */\nconst ROLE_WHAT: Record<Role, string> = {\n viewer: 'reads everything',\n developer: 'deploys and tears down',\n maintainer: 'host configuration',\n admin: 'accounts and sign-on',\n};\nconst roleOptions = ROLES.map((r) => ({ value: r, label: sentence(r), hint: ROLE_WHAT[r] }));\n\nconst adminCount = computed(() => users.value.filter((u) => u.role === 'admin').length);\n/**\n * The last admin cannot be demoted or deleted — the server refuses both, and a host with no admin\n * has nobody who can add one back. Counting ROWS is the same thing the server counts: root holds\n * PSTACK_TOKEN, not an account, and may not exist as a human at all.\n */\nfunction lastAdmin(u: User): boolean {\n return u.role === 'admin' && adminCount.value === 1;\n}\nconst LAST_ADMIN_WHY = 'The only admin — promote another account first.';\n\nasync function load(): Promise<void> {\n const [u, t] = await Promise.all([\n api.get<UsersResponse>('/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 // The role is always sent, never left to the server's default: what this form creates should be\n // whatever the form is showing. (An absent role means viewer there, which is the same answer —\n // but the day it is not, this page would be creating something other than what it said.)\n const r = await api.post<{ user: User }>('/api/users', {\n username: newUser.value.username.trim(),\n password: newUser.value.password,\n role: newUser.value.role,\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this account');\n return;\n }\n toast('ok', `Added ${r.body.user.username}.`);\n newUser.value = { username: '', password: '', role: 'viewer' };\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 changeRole(u: User, role: string): Promise<void> {\n if (role === u.role) return;\n const r = await api.patch(`/api/users/${u.id}`, { role });\n if (!r.ok) {\n listError.value = problem(r, `change the role for ${u.username}`);\n return;\n }\n listError.value = '';\n if (u.id === authState.user?.id) {\n // You just changed your OWN role, and this whole app — the rail, this page's controls, the\n // footer — is drawn from the role it is holding. Re-read it before anything renders again,\n // or the UI keeps offering what the server will now refuse. (The server needs no prompting:\n // every request looks the role up fresh.)\n await checkAuth();\n toast('ok', `You are now a ${role}.`);\n } else {\n toast('ok', `${u.username} is now a ${role}.`);\n }\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 // If it was YOU, the next request is a 401 — the toast says to sign in again so that reads as an\n // expected step rather than a bug. The dialog carried the full warning before the change.\n toast(\n 'ok',\n u.id === authState.user?.id ? `Changed — sign in again.` : `Password changed.`,\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: 'Shown once — the server keeps only a hash. It can do anything your account can.',\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 & access</h1>\n <div class=\"sub\">Accounts and personal tokens</div>\n </div>\n <span class=\"grow\" />\n <RefreshButton :run=\"load\" />\n </div>\n\n <ErrorNote v-if=\"listError\" :text=\"listError\" title=\"Could not load the accounts.\" />\n\n <div v-if=\"revealed\" class=\"banner ok\">\n <b>{{ revealed.what }} — copy it now.</b>\n <!-- The one-line interlock: there is no read path on the server, so this really is the only\n time the value exists anywhere but the clipboard. -->\n <p>{{ revealed.note }}</p>\n <pre class=\"code\">{{ revealed.value }}</pre>\n <div class=\"row\">\n <button @click=\"revealed = null\">Dismiss</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\" tall />\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\" role=\"table\" class=\"cards\">\n <thead role=\"rowgroup\">\n <tr role=\"row\">\n <th role=\"columnheader\">User</th>\n <th role=\"columnheader\">Role</th>\n <th role=\"columnheader\">Added</th>\n <th role=\"columnheader\" aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody role=\"rowgroup\">\n <tr v-for=\"u in users\" :key=\"u.id\" role=\"row\">\n <td role=\"cell\" 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 <!-- An admin gets the picker in place: promotion is a one-step decision about one\n person, and a dialog for a four-option choice is a page-blocking ceremony. Anyone\n else reads the role as text — `sentence()` at render only, never on the value.\n A role this build does not know still shows: it falls through to `placeholder`. -->\n <td role=\"cell\" data-label=\"role\">\n <span\n v-if=\"can('admin')\"\n class=\"role-pick\"\n :title=\"lastAdmin(u) ? LAST_ADMIN_WHY : undefined\"\n >\n <SelectMenu\n :model-value=\"u.role\"\n :label=\"`Role for ${u.username}`\"\n :options=\"roleOptions\"\n :disabled=\"lastAdmin(u)\"\n :placeholder=\"sentence(u.role)\"\n @update:model-value=\"(v) => changeRole(u, v)\"\n />\n </span>\n <span v-else>{{ sentence(u.role) }}</span>\n </td>\n <td role=\"cell\" class=\"dim nowrap\" data-label=\"added\"><RelativeTime :at=\"u.createdAt\" /></td>\n <td role=\"cell\" class=\"right nowrap\" data-label=\"\">\n <!-- Your own password is yours at any role; someone else's is an admin operation. -->\n <button\n v-if=\"u.id === authState.user?.id || can('admin')\"\n class=\"ghost sm\"\n @click=\"pwFor = u; pwValue = ''\"\n >\n Change password\n </button>\n <ActionButton\n v-if=\"can('admin')\"\n class=\"danger sm\"\n :confirm=\"`Delete ${u.username}?`\"\n :disabled=\"lastAdmin(u)\"\n :title=\"lastAdmin(u) ? LAST_ADMIN_WHY : undefined\"\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 v-if=\"can('admin')\" 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 <div class=\"field\">\n <label for=\"new-role\">Role</label>\n <SelectMenu\n id=\"new-role\"\n :model-value=\"newUser.role\"\n label=\"Role for the new account\"\n :options=\"roleOptions\"\n @update:model-value=\"(v) => (newUser.role = v as Role)\"\n />\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: '…', role: newUser.role }\"\n />\n </form>\n <!-- Not an apology for a missing button: reading the roster is every account's, changing it\n is admin's, and the roster above already names who to ask. -->\n <p v-else class=\"hint\">Only an admin can add accounts or change roles.</p>\n </section>\n\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Your API tokens</h2>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" tall />\n <table v-else-if=\"tokens.length\" role=\"table\" class=\"cards\">\n <thead role=\"rowgroup\">\n <tr role=\"row\">\n <th role=\"columnheader\">Name</th>\n <th role=\"columnheader\">Last used</th>\n <th role=\"columnheader\" aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody role=\"rowgroup\">\n <tr v-for=\"t in tokens\" :key=\"t.id\" role=\"row\">\n <td role=\"cell\" class=\"name\" data-label=\"name\"><b>{{ t.name }}</b></td>\n <td role=\"cell\" 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 role=\"cell\" class=\"right nowrap\" data-label=\"\">\n <ActionButton\n class=\"danger sm\"\n :confirm=\"`Revoke ${t.name}?`\"\n @run=\"removeToken(t)\"\n >Revoke</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <!-- Two different facts, not one empty list: the host token cannot own tokens at all. -->\n <p v-else class=\"hint\">\n {{ authState.root ? 'The host token has no personal tokens.' : 'No tokens yet — name one below.' }}\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>Name</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. Reka owns\n modality — hand-rolling it declared `aria-modal` without the focus trap it promises. -->\n <DialogRoot :open=\"!!pwFor\" @update:open=\"(v: boolean) => !v && (pwFor = null)\">\n <DialogPortal>\n <DialogOverlay class=\"scrim help-scrim\" />\n <DialogContent v-if=\"pwFor\" class=\"sheet\" aria-label=\"Change password\">\n <DialogTitle as=\"h3\">Change password for {{ pwFor.username }}</DialogTitle>\n <!-- The interlock, one line: this ends every session and revokes every personal token. -->\n <p class=\"hint\">\n Signs {{ pwFor.id === authState.user?.id ? 'you' : 'them' }} out everywhere and revokes\n {{ pwFor.id === authState.user?.id ? 'your' : 'their' }} tokens.\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 <DialogClose class=\"ghost\">Cancel</DialogClose>\n </div>\n </DialogContent>\n </DialogPortal>\n </DialogRoot>\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/* The picker sits in a row, not a form: it takes the width its longest label needs and no more,\n * so the Role column does not eat the table. The wrapper exists to carry `title` when the control\n * inside it is disabled — a disabled button does not reliably show one of its own. */\n.role-pick {\n display: inline-block;\n}\n.role-pick :deep(.selm-trigger) {\n width: auto;\n min-width: 150px;\n}\n</style>\n"],"names":["LAST_ADMIN_WHY","users","ref","tokens","loaded","listError","newUser","newToken","pwFor","pwValue","revealed","canCreateUser","computed","ROLE_WHAT","roleOptions","ROLES","r","sentence","adminCount","u","lastAdmin","load","t","api","problem","createUser","toast","removeUser","changeRole","role","_a","authState","checkAuth","changePassword","createToken","removeToken","_createElementBlock","_createElementVNode","_hoisted_1","_createVNode","RefreshButton","_createBlock","ErrorNote","_openBlock","_hoisted_2","_toDisplayString","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_cache","_hoisted_8","_hoisted_9","_hoisted_10","_Fragment","_renderList","_hoisted_11","_unref","_hoisted_12","_hoisted_13","can","SelectMenu","v","_hoisted_15","_hoisted_16","RelativeTime","_hoisted_17","$event","_hoisted_18","ActionButton","_hoisted_19","SkeletonList","_hoisted_20","_hoisted_21","_vModelText","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","EquivalentCommand","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","_hoisted_31","_hoisted_32","_hoisted_33","_hoisted_34","_hoisted_35","_hoisted_36","_hoisted_37","DialogRoot","DialogPortal","DialogOverlay","DialogContent","DialogTitle","_hoisted_38","_hoisted_39","_hoisted_40","_hoisted_41","DialogClose"],"mappings":"+6DA2FMA,EAAiB,sFAvCvB,MAAMC,EAAQC,EAAY,EAAE,EACtBC,EAASD,EAAa,EAAE,EACxBE,EAASF,EAAI,EAAK,EAClBG,EAAYH,EAAI,EAAE,EAElBI,EAAUJ,EAAwD,CACtE,SAAU,GACV,SAAU,GACV,KAAM,QAAA,CACP,EACKK,EAAWL,EAAI,EAAE,EACjBM,EAAQN,EAAiB,IAAI,EAC7BO,EAAUP,EAAI,EAAE,EAGhBQ,EAAWR,EAA0D,IAAI,EAEzES,EAAgBC,EACpB,IAAMN,EAAQ,MAAM,SAAS,KAAA,EAAO,OAAS,GAAKA,EAAQ,MAAM,SAAS,QAAU,CAAA,EAI/EO,EAAkC,CACtC,OAAQ,mBACR,UAAW,yBACX,WAAY,qBACZ,MAAO,sBAAA,EAEHC,EAAcC,GAAM,IAAKC,IAAO,CAAE,MAAOA,EAAG,MAAOC,EAASD,CAAC,EAAG,KAAMH,EAAUG,CAAC,GAAI,EAErFE,EAAaN,EAAS,IAAMX,EAAM,MAAM,OAAQkB,GAAMA,EAAE,OAAS,OAAO,EAAE,MAAM,EAMtF,SAASC,EAAUD,EAAkB,CACnC,OAAOA,EAAE,OAAS,SAAWD,EAAW,QAAU,CACpD,CAGA,eAAeG,GAAsB,CACnC,KAAM,CAACF,EAAGG,CAAC,EAAI,MAAM,QAAQ,IAAI,CAC/BC,EAAI,IAAmB,YAAY,EACnCA,EAAI,IAAyB,aAAa,CAAA,CAC3C,EAED,GADAnB,EAAO,MAAQ,GACX,CAACe,EAAE,GAAI,CACTd,EAAU,MAAQmB,EAAQL,EAAG,mBAAmB,EAChD,MACF,CACAd,EAAU,MAAQ,GAClBJ,EAAM,MAAQkB,EAAE,KAAK,OAAS,CAAA,EAE9BhB,EAAO,MAAQmB,EAAE,GAAMA,EAAE,KAAK,QAAU,CAAA,EAAM,CAAA,CAChD,CACKD,EAAA,EAEL,eAAeI,GAA4B,CAIzC,MAAMT,EAAI,MAAMO,EAAI,KAAqB,aAAc,CACrD,SAAUjB,EAAQ,MAAM,SAAS,KAAA,EACjC,SAAUA,EAAQ,MAAM,SACxB,KAAMA,EAAQ,MAAM,IAAA,CACrB,EACD,GAAI,CAACU,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,qBAAqB,EAClD,MACF,CACAU,EAAM,KAAM,SAASV,EAAE,KAAK,KAAK,QAAQ,GAAG,EAC5CV,EAAQ,MAAQ,CAAE,SAAU,GAAI,SAAU,GAAI,KAAM,QAAA,EAC/Ce,EAAA,CACP,CAEA,eAAeM,EAAWR,EAAwB,CAChD,MAAMH,EAAI,MAAMO,EAAI,IAAI,cAAcJ,EAAE,EAAE,EAAE,EAG5C,GAAI,CAACH,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,UAAUG,EAAE,QAAQ,EAAE,EACnD,MACF,CACAO,EAAM,KAAM,WAAWP,EAAE,QAAQ,GAAG,EAC/BE,EAAA,CACP,CAEA,eAAeO,EAAWT,EAASU,EAA6B,OAC9D,GAAIA,IAASV,EAAE,KAAM,OACrB,MAAMH,EAAI,MAAMO,EAAI,MAAM,cAAcJ,EAAE,EAAE,GAAI,CAAE,KAAAU,EAAM,EACxD,GAAI,CAACb,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,uBAAuBG,EAAE,QAAQ,EAAE,EAChE,MACF,CACAd,EAAU,MAAQ,GACdc,EAAE,OAAOW,EAAAC,EAAU,OAAV,YAAAD,EAAgB,KAK3B,MAAME,GAAA,EACNN,EAAM,KAAM,iBAAiBG,CAAI,GAAG,GAEpCH,EAAM,KAAM,GAAGP,EAAE,QAAQ,aAAaU,CAAI,GAAG,EAE1CR,EAAA,CACP,CAEA,eAAeY,GAAgC,OAC7C,MAAMd,EAAIX,EAAM,MAChB,GAAI,CAACW,EAAG,OACR,MAAMH,EAAI,MAAMO,EAAI,IAAI,cAAcJ,EAAE,EAAE,YAAa,CAAE,SAAUV,EAAQ,KAAA,CAAO,EAClF,GAAI,CAACO,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,2BAA2BG,EAAE,QAAQ,EAAE,EACpE,MACF,CACAX,EAAM,MAAQ,KACdC,EAAQ,MAAQ,GAGhBiB,EACE,KACAP,EAAE,OAAOW,EAAAC,EAAU,OAAV,YAAAD,EAAgB,IAAK,2BAA6B,mBAAA,EAExDT,EAAA,CACP,CAEA,eAAea,IAA6B,CAC1C,MAAMlB,EAAI,MAAMO,EAAI,KAAoC,cAAe,CACrE,KAAMhB,EAAS,MAAM,KAAA,CAAK,CAC3B,EACD,GAAI,CAACS,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,mBAAmB,EAChD,MACF,CACAN,EAAS,MAAQ,CACf,KAAM,mBAAmBH,EAAS,MAAM,MAAM,IAC9C,MAAOS,EAAE,KAAK,MACd,KAAM,iFAAA,EAERT,EAAS,MAAQ,GACZc,EAAA,CACP,CAEA,eAAec,GAAYb,EAAyB,CAClD,MAAMN,EAAI,MAAMO,EAAI,IAAI,eAAeD,EAAE,EAAE,EAAE,EAC7C,GAAI,CAACN,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,UAAUM,EAAE,IAAI,EAAE,EAC/C,MACF,CACAI,EAAM,KAAM,WAAWJ,EAAE,IAAI,GAAG,EAC3BD,EAAA,CACP,mBAIEe,EAuNM,MAAA,KAAA,CAtNJC,EAOM,MAPNC,GAOM,aANJD,EAGM,MAAA,KAAA,CAFJA,EAA2B,UAAvB,gBAAkB,EACtBA,EAAmD,MAAA,CAA9C,MAAM,KAAA,EAAM,8BAA4B,CAAA,mBAE/CA,EAAqB,OAAA,CAAf,MAAM,MAAA,EAAM,KAAA,EAAA,GAClBE,EAA6BC,GAAA,CAAb,IAAKnB,EAAI,CAAA,GAGVhB,EAAA,WAAjBoC,EAAqFC,GAAA,OAAxD,KAAMrC,EAAA,MAAW,MAAM,8BAAA,6BAEzCK,EAAA,OAAXiC,EAAA,EAAAP,EASM,MATNQ,GASM,CARJP,EAAyC,IAAA,KAAAQ,EAAnCnC,EAAA,MAAS,IAAI,EAAG,kBAAe,CAAA,EAGrC2B,EAA0B,IAAA,KAAAQ,EAApBnC,EAAA,MAAS,IAAI,EAAA,CAAA,EACnB2B,EAA4C,MAA5CS,GAA4CD,EAAvBnC,EAAA,MAAS,KAAK,EAAA,CAAA,EACnC2B,EAEM,MAFNU,GAEM,CADJV,EAAiD,SAAA,CAAxC,uBAAO3B,EAAA,MAAQ,KAAA,EAAS,SAAO,CAAA,eAI5C2B,EAqKM,MArKNW,GAqKM,CApKJX,EA4GU,UA5GVY,GA4GU,CA3GRZ,EAIM,MAJNa,GAIM,CAHJC,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAd,EAAiC,KAAA,CAA7B,MAAM,SAAA,EAAU,WAAQ,EAAA,iBAC5BA,EAAqB,OAAA,CAAf,MAAM,MAAA,EAAM,KAAA,EAAA,GAClBA,EAA2E,OAA3Ee,GAA2EP,EAAtB5C,EAAA,MAAM,MAAM,EAAA,CAAA,CAAA,GAG9CG,EAAA,MAGHH,EAAA,MAAM,QAAxB0C,EAAA,EAAAP,EA0DQ,QA1DRiB,GA0DQ,eAzDNhB,EAOQ,QAAA,CAPD,KAAK,YAAU,CACpBA,EAKK,KAAA,CALD,KAAK,OAAK,CACZA,EAAiC,KAAA,CAA7B,KAAK,cAAA,EAAe,MAAI,EAC5BA,EAAiC,KAAA,CAA7B,KAAK,cAAA,EAAe,MAAI,EAC5BA,EAAkC,KAAA,CAA9B,KAAK,cAAA,EAAe,OAAK,EAC7BA,EAA+C,KAAA,CAA3C,KAAK,eAAe,aAAW,SAAA,WAGvCA,EAgDQ,QAhDRiB,GAgDQ,QA/CNlB,EA8CKmB,EAAA,KAAAC,EA9CWvD,EAAA,MAALkB,GAAC,oBAAZiB,EA8CK,KAAA,CA9CmB,IAAKjB,EAAE,GAAI,KAAK,KAAA,GACtCkB,EAGK,KAHLoB,GAGK,CAFHpB,EAAuB,IAAA,KAAAQ,EAAjB1B,EAAE,QAAQ,EAAA,CAAA,EACJA,EAAE,OAAOuC,EAAAA,KAAU,OAAVA,YAAAA,EAAgB,SAArCtB,EAAsE,OAAtEuB,GAA4D,KAAG,cAMjEtB,EAgBK,KAhBLuB,GAgBK,CAdKF,EAAAG,CAAA,EAAG,OAAA,OADXzB,EAaO,OAAA,OAXL,MAAM,YACL,MAAOhB,EAAUD,CAAC,EAAInB,EAAiB,MAAA,GAExCuC,EAOEuB,EAAA,CANC,cAAa3C,EAAE,KACf,MAAK,YAAcA,EAAE,QAAQ,GAC7B,QAASuC,EAAA5C,CAAA,EACT,SAAUM,EAAUD,CAAC,EACrB,YAAauC,EAAAzC,CAAA,EAASE,EAAE,IAAI,EAC5B,sBAAqB4C,GAAMnC,EAAWT,EAAG4C,CAAC,CAAA,oGAG/CpB,EAAA,EAAAP,EAA0C,OAAA4B,GAAAnB,EAA1Ba,EAAAzC,CAAA,EAASE,EAAE,IAAI,CAAA,EAAA,CAAA,EAAA,GAEjCkB,EAA6F,KAA7F4B,GAA6F,CAAvC1B,EAAkC2B,EAAA,CAAnB,GAAI/C,EAAE,SAAA,mBAC3EkB,EAkBK,KAlBL8B,GAkBK,CAfKhD,EAAE,OAAOuC,EAAAA,EAAA3B,CAAA,EAAU,OAAV2B,YAAAA,EAAgB,KAAMA,EAAAG,CAAA,EAAG,OAAA,OAD1CzB,EAMS,SAAA,OAJP,MAAM,WACL,QAAKgC,GAAA,CAAE5D,EAAA,MAAQW,EAAGV,EAAA,MAAO,EAAA,CAAA,EAC3B,oBAED,EAAA4D,EAAA,YAEQX,EAAAG,CAAA,EAAG,OAAA,OADXpB,EAQC6B,EAAA,OANC,MAAM,YACL,QAAO,UAAYnD,EAAE,QAAQ,IAC7B,SAAUC,EAAUD,CAAC,EACrB,MAAOC,EAAUD,CAAC,EAAInB,EAAiB,OACvC,MAAGoE,GAAEzC,EAAWR,CAAC,CAAA,aACjB,IAAM,CAAA,GAAAgC,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAA,GAAN,SAAM,EAAA,CAAA,oFAMjBf,EAA2C,IAA3CmC,GAAuB,kBAAgB,QA9DvC9B,EAA8C+B,EAAA,OAAhB,KAAM,EAAG,KAAA,EAAA,IAgE3Bd,EAAAG,CAAA,EAAG,OAAA,OAAfzB,EAiCO,OAAA,OAjCmB,MAAM,aAAc,WAAgBX,EAAU,CAAA,SAAA,CAAA,CAAA,GACtE0B,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAuB,UAAnB,iBAAc,EAAA,GAClBA,EASM,MATNoC,GASM,CARJpC,EAGQ,QAHRqC,GAGQ,CAFNvB,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAqB,YAAf,WAAQ,EAAA,KACdA,EAAmE,QAAA,CAAnD,sBAAAc,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAiB,GAAA9D,EAAA,MAAQ,SAAQ8D,GAAE,KAAK,OAAO,aAAa,KAAA,aAA3C,CAAAO,EAAArE,EAAA,MAAQ,QAAQ,CAAA,KAElC+B,EAGQ,QAHRuC,GAGQ,CAFNzB,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAqB,YAAf,WAAQ,EAAA,KACdA,EAAgF,QAAA,CAAhE,sBAAAc,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAiB,GAAA9D,EAAA,MAAQ,SAAQ8D,GAAE,KAAK,WAAW,aAAa,cAAA,aAA/C,CAAAO,EAAArE,EAAA,MAAQ,QAAQ,CAAA,OAGpC+B,EASM,MATNwC,GASM,CARJ1B,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAkC,QAAA,CAA3B,IAAI,UAAA,EAAW,OAAI,EAAA,GAC1BE,EAMEuB,EAAA,CALA,GAAG,WACF,cAAaxD,EAAA,MAAQ,KACtB,MAAM,2BACL,QAASoD,EAAA5C,CAAA,EACT,kCAAqBiD,GAAOzD,QAAQ,KAAOyD,EAAA,sCAGhDZ,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAA0C,IAAA,CAAvC,MAAM,MAAA,EAAO,yBAAsB,EAAA,GACtCA,EAGM,MAHNyC,GAGM,CAFJzC,EAAqF,SAAA,CAA7E,MAAM,UAAU,KAAK,SAAU,UAAW1B,EAAA,KAAA,EAAe,cAAW,EAAAoE,EAAA,gBAC5E1C,EAAqB,OAAA,CAAf,MAAM,QAAM,KAAA,EAAA,EAAA,GAEpBE,EAKEyC,EAAA,CAJA,KAAK,wBACL,OAAO,OACP,KAAK,aACJ,eAAkB1E,EAAA,MAAQ,UAAQ,QAAA,SAAA,IAAA,KAAkCA,EAAA,MAAQ,IAAA,CAAI,8BAKrF8B,EAA0E,IAA1E6C,GAAuB,iDAA+C,EAAA,GAGxE5C,EAqDU,UArDV6C,GAqDU,eApDR7C,EAEM,MAAA,CAFD,MAAM,SAAO,CAChBA,EAAwC,KAAA,CAApC,MAAM,SAAA,EAAU,iBAAe,CAAA,OAGhBjC,EAAA,MACHD,EAAA,MAAO,QAAzBwC,EAAA,EAAAP,EAyBQ,QAzBR+C,GAyBQ,eAxBN9C,EAMQ,QAAA,CAND,KAAK,YAAU,CACpBA,EAIK,KAAA,CAJD,KAAK,OAAK,CACZA,EAAiC,KAAA,CAA7B,KAAK,cAAA,EAAe,MAAI,EAC5BA,EAAsC,KAAA,CAAlC,KAAK,cAAA,EAAe,WAAS,EACjCA,EAA+C,KAAA,CAA3C,KAAK,eAAe,aAAW,SAAA,WAGvCA,EAgBQ,QAhBR+C,GAgBQ,QAfNhD,EAcKmB,EAAA,KAAAC,EAdWrD,EAAA,MAALmB,QAAXc,EAcK,KAAA,CAdoB,IAAKd,EAAE,GAAI,KAAK,KAAA,GACvCe,EAAuE,KAAvEgD,GAAuE,CAAxBhD,EAAmB,IAAA,KAAAQ,EAAbvB,EAAE,IAAI,EAAA,CAAA,CAAA,GAC3De,EAGK,KAHLiD,GAGK,CAFiBhE,EAAE,gBAAtBmB,EAAuDyB,EAAA,OAApB,GAAI5C,EAAE,UAAA,uBACzCc,EAAsC,OAAtCmD,GAA0B,OAAK,EAAA,GAEjClD,EAOK,KAPLmD,GAOK,CANHjD,EAKC+B,EAAA,CAJC,MAAM,YACL,QAAO,UAAYhD,EAAE,IAAI,IACzB,MAAG8C,GAAEjC,GAAYb,CAAC,CAAA,aAClB,IAAM,CAAA,GAAA6B,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAA,GAAN,SAAM,EAAA,CAAA,mDAOjBR,EAAA,EAAAP,EAEI,IAFJqD,GAEI5C,EADCa,EAAA3B,CAAA,EAAU,KAAI,yCAAA,iCAAA,EAAA,CAAA,QA7BnBU,EAA8C+B,EAAA,OAAhB,KAAM,EAAG,KAAA,EAAA,IAgC1Bd,EAAA3B,CAAA,EAAU,mBAAvBK,EAeO,OAAA,OAfsB,MAAM,aAAc,WAAgBF,GAAW,CAAA,SAAA,CAAA,CAAA,GAC1EiB,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAkB,UAAd,YAAS,EAAA,GACbA,EAGQ,QAHRqD,GAGQ,CAFNvC,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAiB,YAAX,OAAI,EAAA,KACVA,EAAmF,QAAA,sCAAnE9B,EAAQ,MAAA6D,GAAE,KAAK,OAAO,YAAY,YAAY,aAAa,KAAA,gBAA3D7D,EAAA,KAAQ,CAAA,KAE1B8B,EAEM,MAFNsD,GAEM,CADJtD,EAAwF,SAAA,CAAhF,MAAM,UAAU,KAAK,SAAU,SAAQ,CAAG9B,EAAA,MAAS,KAAA,CAAI,EAAI,eAAY,EAAAqF,EAAA,CAAA,GAEjFrD,EAKEyC,EAAA,CAJA,KAAK,sBACL,OAAO,OACP,KAAK,cACJ,WAAczE,EAAA,OAAQ,WAAA,CAAA,6BAQ/BgC,EAsBamB,EAAAmC,EAAA,EAAA,CAtBA,OAAQrF,EAAA,MAAQ,gBAAW2C,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAGY,GAAU,CAAMA,IAAMvD,EAAA,MAAK,MAAA,aACpE,IAoBe,CApBf+B,EAoBemB,EAAAoC,EAAA,EAAA,KAAA,WAnBb,IAA0C,CAA1CvD,EAA0CmB,EAAAqC,EAAA,EAAA,CAA3B,MAAM,mBAAkB,EAClBvF,EAAA,WAArBiC,EAiBgBiB,EAAAsC,EAAA,EAAA,OAjBY,MAAM,QAAQ,aAAW,iBAAA,aACnD,IAAA,SAA2E,OAA3EzD,EAA2EmB,EAAAuC,EAAA,EAAA,CAA9D,GAAG,MAAI,WAAC,IAAoB,GAApB,uBAAoBpD,EAAGrC,EAAA,MAAM,QAAQ,EAAA,CAAA,CAAA,SAE1D6B,EAGI,IAHJ6D,GAAgB,UACRrD,EAAGrC,EAAA,MAAM,OAAOkD,EAAAA,EAAA3B,CAAA,EAAU,OAAV2B,YAAAA,EAAgB,IAAE,MAAA,MAAA,EAAoB,+BAC5Db,EAAGrC,EAAA,MAAM,OAAOkD,EAAAA,EAAA3B,CAAA,EAAU,OAAV2B,YAAAA,EAAgB,IAAE,OAAA,OAAA,EAAsB,YAC1D,CAAA,EACArB,EAGQ,QAHR8D,GAGQ,CAFNhD,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAyB,YAAnB,eAAY,EAAA,KAClBA,EAAuE,QAAA,sCAAvD5B,EAAO,MAAA2D,GAAE,KAAK,WAAW,aAAa,cAAA,gBAAtC3D,EAAA,KAAO,CAAA,KAEzB4B,EAKM,MALN+D,GAKM,CAJJ/D,EAES,SAAA,CAFD,MAAM,UAAW,SAAU5B,EAAA,MAAQ,OAAM,EAAO,QAAOwB,CAAA,EAAgB,oBAE/E,EAAAoE,EAAA,EACA9D,EAA+CmB,EAAA4C,EAAA,EAAA,CAAlC,MAAM,SAAO,WAAC,IAAM,CAAA,GAAAnD,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAA,GAAN,SAAM,EAAA,CAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{V as f,m as k}from"./VarIO-
|
|
2
|
-
//# sourceMappingURL=VarEditor.vue_vue_type_script_setup_true_lang-
|
|
1
|
+
import{V as f,m as k}from"./VarIO-BFBaI8oU.js";import{d as b,c as p,a as l,F as g,r as x,e as y,h as _,o as c,n as m}from"./index-D_Ks6aQi.js";const w=["value","aria-label","onInput"],I=["value","aria-label","onInput"],C=["aria-label","onClick"],N=b({__name:"VarEditor",props:{modelValue:{}},emits:["update:modelValue","change"],setup(r,{emit:i}){const u=r,t=i;function d(o,a){const n=u.modelValue.map((e,s)=>s===o?{...e,...a}:e);t("update:modelValue",n),t("change")}function v(){t("update:modelValue",[...u.modelValue,{k:"",v:""}]),t("change")}function h(o,a){const n=a==="replace"?o:k(u.modelValue,o);t("update:modelValue",n.length?n:[{k:"",v:""}]),t("change")}function V(o){const a=u.modelValue.filter((n,e)=>e!==o);t("update:modelValue",a.length?a:[{k:"",v:""}]),t("change")}return(o,a)=>(c(),p("div",null,[a[1]||(a[1]=l("div",{class:"kv head"},[l("span",null,"name"),l("span",null,"value"),l("span")],-1)),(c(!0),p(g,null,x(r.modelValue,(n,e)=>(c(),p("div",{key:e,class:"kv"},[l("input",{type:"text",value:n.k,"aria-label":`variable name ${e+1}`,placeholder:"PR",spellcheck:"false",autocomplete:"off",onInput:s=>d(e,{k:s.target.value.trim()})},null,40,w),l("input",{type:"text",value:n.v,"aria-label":`variable value ${e+1}`,placeholder:"7",spellcheck:"false",autocomplete:"off",onInput:s=>d(e,{v:s.target.value})},null,40,I),l("button",{class:"sm ghost","aria-label":`remove variable ${e+1}`,onClick:s=>V(e)},"−",8,C)]))),128)),l("button",{class:"sm",onClick:v},"+ variable"),y(f,{pairs:r.modelValue,replace:"",onApply:h},{default:_(()=>[...a[0]||(a[0]=[l("p",{class:"hint",style:{"margin-top":"0"}},[l("b",null,"Replace all"),m(" uses exactly what was pasted. "),l("b",null,"Merge"),m(" overwrites the names it finds, keeps the rest, and adds what is new. ")],-1)])]),_:1},8,["pairs"])]))}});export{N as _};
|
|
2
|
+
//# sourceMappingURL=VarEditor.vue_vue_type_script_setup_true_lang-BNQTEru_.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VarEditor.vue_vue_type_script_setup_true_lang-
|
|
1
|
+
{"version":3,"file":"VarEditor.vue_vue_type_script_setup_true_lang-BNQTEru_.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 *\n * The rows are the editor; `VarIO` under them is the same list as `.env`, CSV or TSV. Typing a\n * dozen variables one input at a time is how they end up mistyped, and the list an operator\n * already has is almost always one of those three formats.\n */\nimport type { VarPair } from '../api/types';\nimport { mergeVars } from '../composables/useVarFormats';\nimport VarIO from './VarIO.vue';\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\n/**\n * Both modes are honest here — this editor owns its whole list, so replacing it is a thing the\n * operator can mean. A replace that parsed to nothing still leaves one blank row: an editor with no\n * rows looks broken.\n */\nfunction applyImport(pairs: VarPair[], mode: 'replace' | 'merge'): void {\n const next = mode === 'replace' ? pairs : mergeVars(props.modelValue, pairs);\n emit('update:modelValue', next.length ? next : [{ 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\n <VarIO :pairs=\"modelValue\" replace @apply=\"applyImport\">\n <p class=\"hint\" style=\"margin-top: 0\">\n <b>Replace all</b> uses exactly what was pasted. <b>Merge</b> overwrites the names it finds,\n keeps the rest, and adds what is new.\n </p>\n </VarIO>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","set","i","patch","next","n","add","applyImport","pairs","mode","mergeVars","remove","_","_createElementBlock","_createElementVNode","_openBlock","_Fragment","_renderList","e","$event","_hoisted_3","_createVNode","VarIO","_cache"],"mappings":"2VAgBA,MAAMA,EAAQC,EACRC,EAAOC,EAEb,SAASC,EAAIC,EAAWC,EAA+B,CACrD,MAAMC,EAAOP,EAAM,WAAW,IAAI,CAAC,EAAGQ,IAAOA,IAAMH,EAAI,CAAE,GAAG,EAAG,GAAGC,CAAA,EAAU,CAAE,EAC9EJ,EAAK,oBAAqBK,CAAI,EAC9BL,EAAK,QAAQ,CACf,CAEA,SAASO,GAAY,CACnBP,EAAK,oBAAqB,CAAC,GAAGF,EAAM,WAAY,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEE,EAAK,QAAQ,CACf,CAOA,SAASQ,EAAYC,EAAkBC,EAAiC,CACtE,MAAML,EAAOK,IAAS,UAAYD,EAAQE,EAAUb,EAAM,WAAYW,CAAK,EAC3ET,EAAK,oBAAqBK,EAAK,OAASA,EAAO,CAAC,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEL,EAAK,QAAQ,CACf,CAEA,SAASY,EAAOT,EAAiB,CAC/B,MAAME,EAAOP,EAAM,WAAW,OAAO,CAACe,EAAGP,IAAMA,IAAMH,CAAC,EAEtDH,EAAK,oBAAqBK,EAAK,OAASA,EAAO,CAAC,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEL,EAAK,QAAQ,CACf,mBAIEc,EA+BM,MAAA,KAAA,aA9BJC,EAAsE,MAAA,CAAjE,MAAM,WAAS,CAACA,EAAiB,YAAX,MAAI,EAAOA,EAAkB,YAAZ,OAAK,EAAOA,EAAQ,MAAA,CAAA,QAChEC,EAAA,EAAA,EAAAF,EAoBMG,EAAA,KAAAC,EApBgBnB,EAAA,WAAU,CAAnBoB,EAAGhB,SAAhBW,EAoBM,MAAA,CApB6B,IAAKX,EAAG,MAAM,IAAA,GAC/CY,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,8BAA6BhB,EAAC,CAAA,GAC/B,YAAY,KACZ,WAAW,QACX,aAAa,MACZ,QAAKiB,GAAElB,EAAIC,EAAC,CAAA,EAAQiB,EAAO,OAA4B,MAAM,MAAI,CAAA,CAAA,aAEpEL,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,+BAA8BhB,EAAC,CAAA,GAChC,YAAY,IACZ,WAAW,QACX,aAAa,MACZ,QAAKiB,GAAElB,EAAIC,KAASiB,EAAO,OAA4B,KAAA,CAAK,CAAA,aAE/DL,EAA+F,SAAA,CAAvF,MAAM,WAAY,gCAA+BZ,EAAC,CAAA,GAAS,QAAKiB,GAAER,EAAOT,CAAC,CAAA,EAAG,IAAC,EAAAkB,CAAA,CAAA,WAExFN,EAAmD,SAAA,CAA3C,MAAM,KAAM,QAAOR,CAAA,EAAK,YAAU,EAE1Ce,EAKQC,EAAA,CALA,MAAOxB,EAAA,WAAY,QAAA,GAAS,QAAOS,CAAA,aACzC,IAGI,CAAA,GAAAgB,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAA,CAHJT,EAGI,IAAA,CAHD,MAAM,OAAO,MAAA,CAAA,aAAA,GAAA,CAAA,GACdA,EAAkB,SAAf,aAAW,IAAI,iCAA+B,EAAAA,EAAY,SAAT,OAAK,IAAI,wEAE/D,CAAA"}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import{d as P,c as h,a as c,F as $,r as S,u as C,t as b,
|
|
1
|
+
import{d as P,c as h,a as c,F as $,r as S,u as C,t as b,f as w,Y as Z,g as Q,v as W,e as q,n as M,E as H,i as V,j as A,o as m,a6 as N}from"./index-D_Ks6aQi.js";import{_ as U}from"./SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js";import{_ as G}from"./_plugin-vue_export-helper-DlAUqK2U.js";const T=["env","csv","tsv"],x={env:".env",csv:"CSV",tsv:"TSV"},D=/^[A-Za-z_][A-Za-z0-9_]*$/,Y=/^[A-Za-z0-9_@%+=:,./-]+$/,F={csv:",",tsv:" "},J=new Set(["name","key","variable","var"]),K=new Set(["value","val"]);function X(e,s,t){const a=e.filter(l=>l.k);if(s==="env")return[...t?t.split(`
|
|
2
2
|
`).map(o=>`# ${o}`):[],...a.map(o=>`${o.k}=${ee(o.v)}`)].join(`
|
|
3
3
|
`)+`
|
|
4
4
|
`;const n=F[s];return[["name","value"],...a.map(l=>[l.k,l.v])].map(l=>l.map(o=>te(o??"",n)).join(n)).join(`
|
|
5
5
|
`)+`
|
|
6
|
-
`}function ee(e){return e===""||
|
|
6
|
+
`}function ee(e){return e===""||Y.test(e)?e:`"${e.replace(/[\\"]/g,"\\$&").replace(/\n/g,"\\n").replace(/\r/g,"\\r")}"`}function te(e,s){return e===""||!e.includes(s)&&!e.includes('"')&&!/[\n\r]/.test(e)&&e===e.trim()?e:`"${e.replace(/"/g,'""')}"`}function ne(e,s){const t=s??se(e);return t==="env"?ae(e):ie(e,t)}function se(e){const s=e.split(/\r\n|\n|\r/).find(n=>n.trim()&&!n.trim().startsWith("#"))??"";if(/^(export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=/.test(s.trim()))return"env";const t=L(e," "),a=L(e,",");return t<=0&&a<=0?"env":t>a?"tsv":"csv"}function L(e,s){let t=0;for(const a of z(e,s)){const n=B(a.fields);if(n.length){t+=n.length===2?1:-1;for(const l of n)l.includes('"')&&(t-=1)}}return t}function ae(e){const s=e.split(/\r\n|\n|\r/),t=[],a=[];for(let n=0;n<s.length;n++){const l=s[n]??"",o=n+1;if(!l.trim()||l.trim().startsWith("#"))continue;const p=l.trimStart().replace(/^export\s+/,""),f=p.indexOf("=");if(f<0){a.push({line:o,text:l,reason:'no "=" — a line reads NAME=value'});continue}const i=p.slice(0,f).trim();if(!D.test(i)){a.push({line:o,text:l,reason:I(i)});continue}const y=p.slice(f+1).replace(/^[ \t]+/,""),d=y[0];if(d==='"'||d==="'"){const k=n;let g=y.slice(1),_=O(g,d);for(;_<0&&n+1<s.length;)n++,g+=`
|
|
7
7
|
`+(s[n]??""),_=O(g,d);if(_<0){n=k,a.push({line:o,text:l,reason:"the quote is never closed"});continue}const E=g.slice(0,_);t.push({k:i,v:d==='"'?oe(E):E});continue}t.push({k:i,v:y.replace(/\s+#.*$/,"").trimEnd()})}return{format:"env",pairs:j(t),problems:a}}const le={n:`
|
|
8
|
-
`,r:"\r",t:" ","\\":"\\",'"':'"',"'":"'",$:"$","`":"`"};function oe(e){let s="";for(let t=0;t<e.length;t++){const a=e[t]??"";if(a!=="\\"||t===e.length-1){s+=a;continue}const n=e[++t]??"";s+=le[n]??"\\"+n}return s}function O(e,s){for(let t=0;t<e.length;t++){const a=e[t];if(s==='"'&&a==="\\"){t++;continue}if(a===s)return t}return-1}function ie(e,s){const t=z(e,F[s]),a=[],n=[];let l=!0;for(const o of t){const p=B(o.fields);if(!p.length)continue;if(l){l=!1;const i=(p[0]??"").toLowerCase(),y=(p[1]??"").toLowerCase();if(p.length===2&&
|
|
8
|
+
`,r:"\r",t:" ","\\":"\\",'"':'"',"'":"'",$:"$","`":"`"};function oe(e){let s="";for(let t=0;t<e.length;t++){const a=e[t]??"";if(a!=="\\"||t===e.length-1){s+=a;continue}const n=e[++t]??"";s+=le[n]??"\\"+n}return s}function O(e,s){for(let t=0;t<e.length;t++){const a=e[t];if(s==='"'&&a==="\\"){t++;continue}if(a===s)return t}return-1}function ie(e,s){const t=z(e,F[s]),a=[],n=[];let l=!0;for(const o of t){const p=B(o.fields);if(!p.length)continue;if(l){l=!1;const i=(p[0]??"").toLowerCase(),y=(p[1]??"").toLowerCase();if(p.length===2&&J.has(i)&&K.has(y))continue}if(p.length!==2){const i=p.length;n.push({line:o.line,text:o.raw,reason:`${i} column${i===1?"":"s"} — expected two, a name and a value`});continue}const f=(p[0]??"").trim();if(!D.test(f)){n.push({line:o.line,text:o.raw,reason:I(f)});continue}a.push({k:f,v:p[1]??""})}return{format:s,pairs:j(a),problems:n}}function z(e,s){const t=e.replace(/\r\n?/g,`
|
|
9
9
|
`),a=[];let n=[],l="",o=!1,p=!1,f=1,i=1,y=0;const d=()=>{n.push(o?l:l.trim()),l="",o=!1};for(let k=0;k<t.length;k++){const g=t[k]??"";if(p){g==='"'?t[k+1]==='"'?(l+='"',k++):p=!1:(g===`
|
|
10
10
|
`&&f++,l+=g);continue}if(g==='"'&&!o&&l.trim()===""){l="",p=!0,o=!0;continue}if(g===s){d();continue}if(g===`
|
|
11
|
-
`){d(),a.push({fields:n,line:i,raw:t.slice(y,k)}),n=[],f++,i=f,y=k+1;continue}l+=g}return(l!==""||o||n.length)&&(d(),a.push({fields:n,line:i,raw:t.slice(y)})),a}function B(e){let s=e.length;for(;s>0&&(e[s-1]??"")==="";)s--;return s===0?[]:e.slice(0,Math.max(s,Math.min(2,e.length)))}function I(e){return e?`"${e}" is not a usable variable name — letters, digits and _, not starting with a digit`:"no name before the value"}function j(e){const s=new Map;for(const t of e)s.set(t.k,t.v);return[...s].map(([t,a])=>({k:t,v:a}))}function Le(e,s){const t=e.filter(a=>a.k).map(a=>({...a}));for(const a of s){const n=t.find(l=>l.k===a.k);n?n.v=a.v:t.push({...a})}return t}const re={class:"vario"},ue={class:"row"},ce=["disabled","title","onClick"],pe=["aria-expanded"],de={key:0,class:"vario-box"},ve=["value"],fe={key:1,class:"vario-box"},he={class:"row",style:{"margin-top":"var(--s2)"}},me={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},be={class:"hint"},ge={key:0,class:"kvlist vario-preview"},ye={class:"k"},ke={class:"v mono"},we={key:0,class:"mute"},_e={key:1,class:"banner warn"},$e={class:"vario-problems"},xe={class:"mute"},Ee={class:"dim"},Ce={class:"row",style:{"margin-top":"var(--s3)"}},Ve=["disabled","title"],Ae=["disabled","title"],R=12,Se=P({__name:"VarIO",props:{pairs:{},note:{},replace:{type:Boolean},skipEmpty:{type:Boolean},busy:{type:Boolean}},emits:["apply"],setup(e,{emit:s}){const t=e,a=s,n=V(!1),l=V(""),o=V("auto"),p=V(""),f=A(()=>t.pairs.filter(v=>v.k).length),i=A(()=>l.value.trim()?ne(l.value,o.value==="auto"?void 0:o.value):null),y=A(()=>t.skipEmpty&&i.value?i.value.pairs.filter(v=>!v.v):[]),d=A(()=>i.value?t.skipEmpty?i.value.pairs.filter(v=>v.v):i.value.pairs:[]),k=[{value:"auto",label:"Detect the format",hint:"read the paste and decide"},...T.map(v=>({value:v,label:`Read as ${x[v]}`}))];async function g(v){const r=
|
|
11
|
+
`){d(),a.push({fields:n,line:i,raw:t.slice(y,k)}),n=[],f++,i=f,y=k+1;continue}l+=g}return(l!==""||o||n.length)&&(d(),a.push({fields:n,line:i,raw:t.slice(y)})),a}function B(e){let s=e.length;for(;s>0&&(e[s-1]??"")==="";)s--;return s===0?[]:e.slice(0,Math.max(s,Math.min(2,e.length)))}function I(e){return e?`"${e}" is not a usable variable name — letters, digits and _, not starting with a digit`:"no name before the value"}function j(e){const s=new Map;for(const t of e)s.set(t.k,t.v);return[...s].map(([t,a])=>({k:t,v:a}))}function Le(e,s){const t=e.filter(a=>a.k).map(a=>({...a}));for(const a of s){const n=t.find(l=>l.k===a.k);n?n.v=a.v:t.push({...a})}return t}const re={class:"vario"},ue={class:"row"},ce=["disabled","title","onClick"],pe=["aria-expanded"],de={key:0,class:"vario-box"},ve=["value"],fe={key:1,class:"vario-box"},he={class:"row",style:{"margin-top":"var(--s2)"}},me={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},be={class:"hint"},ge={key:0,class:"kvlist vario-preview"},ye={class:"k"},ke={class:"v mono"},we={key:0,class:"mute"},_e={key:1,class:"banner warn"},$e={class:"vario-problems"},xe={class:"mute"},Ee={class:"dim"},Ce={class:"row",style:{"margin-top":"var(--s3)"}},Ve=["disabled","title"],Ae=["disabled","title"],R=12,Se=P({__name:"VarIO",props:{pairs:{},note:{},replace:{type:Boolean},skipEmpty:{type:Boolean},busy:{type:Boolean}},emits:["apply"],setup(e,{emit:s}){const t=e,a=s,n=V(!1),l=V(""),o=V("auto"),p=V(""),f=A(()=>t.pairs.filter(v=>v.k).length),i=A(()=>l.value.trim()?ne(l.value,o.value==="auto"?void 0:o.value):null),y=A(()=>t.skipEmpty&&i.value?i.value.pairs.filter(v=>!v.v):[]),d=A(()=>i.value?t.skipEmpty?i.value.pairs.filter(v=>v.v):i.value.pairs:[]),k=[{value:"auto",label:"Detect the format",hint:"read the paste and decide"},...T.map(v=>({value:v,label:`Read as ${x[v]}`}))];async function g(v){const r=X(t.pairs,v,t.note);try{await navigator.clipboard.writeText(r),p.value="",N("ok",`Copied ${f.value} as ${x[v]}.`)}catch{p.value=r,N("error","Could not reach the clipboard — select the text below and copy it.")}}function _(v){a("apply",d.value,v),l.value="",n.value=!1}function E(v){v.target.select()}return(v,r)=>(m(),h("div",re,[c("div",ue,[r[7]||(r[7]=c("span",{class:"mute vario-lbl"},"Copy as",-1)),(m(!0),h($,null,S(C(T),u=>(m(),h("button",{key:u,type:"button",class:"sm ghost",disabled:!f.value,title:f.value?`Copy all ${f.value} as ${C(x)[u]}`:"There is nothing to copy yet",onClick:Re=>g(u)},b(C(x)[u]),9,ce))),128)),r[8]||(r[8]=c("span",{class:"sep","aria-hidden":"true"},null,-1)),c("button",{type:"button",class:"sm","aria-expanded":n.value,onClick:r[0]||(r[0]=u=>n.value=!n.value)},b(n.value?"Close":"Paste to fill"),9,pe)]),p.value?(m(),h("div",de,[r[9]||(r[9]=c("p",{class:"hint"}," The clipboard could not be reached — a browser grants it only on a secure origin, and not always then. Select this and copy it by hand. ",-1)),c("textarea",{value:p.value,readonly:"",rows:"6",spellcheck:"false","aria-label":"the exported variables",onFocus:E},null,40,ve),c("button",{type:"button",class:"sm ghost",onClick:r[1]||(r[1]=u=>p.value="")},"Hide")])):w("",!0),n.value?(m(),h("div",fe,[Z(v.$slots,"default",{},void 0,!0),Q(c("textarea",{"onUpdate:modelValue":r[2]||(r[2]=u=>l.value=u),rows:"7",spellcheck:"false",autocomplete:"off","aria-label":"paste variables",placeholder:`PR=7
|
|
12
12
|
REGION=eu-central
|
|
13
13
|
|
|
14
|
-
…or two columns of CSV or TSV`},null,512),[[
|
|
15
|
-
//# sourceMappingURL=VarIO-
|
|
14
|
+
…or two columns of CSV or TSV`},null,512),[[W,l.value]]),c("div",he,[q(U,{modelValue:o.value,"onUpdate:modelValue":r[3]||(r[3]=u=>o.value=u),label:"how to read the paste",options:k},null,8,["modelValue"]),i.value?(m(),h("span",me," read as "+b(C(x)[i.value.format]),1)):w("",!0)]),i.value?(m(),h($,{key:0},[c("p",be,[c("b",null,b(d.value.length),1),M(" variable"+b(d.value.length===1?"":"s")+" read. ",1),y.value.length?(m(),h($,{key:0},[M(b(y.value.length)+" ("+b(y.value.map(u=>u.k).join(", "))+") had no value and will be skipped — a value stored here is never blank. ",1)],64)):w("",!0)]),d.value.length?(m(),h("ul",ge,[(m(!0),h($,null,S(d.value.slice(0,R),u=>(m(),h("li",{key:u.k},[c("span",ye,[c("code",null,b(u.k),1)]),c("span",ke,b(u.v),1)]))),128)),d.value.length>R?(m(),h("li",we," and "+b(d.value.length-R)+" more ",1)):w("",!0)])):w("",!0),i.value.problems.length?(m(),h("div",_e,[c("b",null,b(i.value.problems.length)+" line"+b(i.value.problems.length===1?"":"s")+" could not be read ",1),c("ul",$e,[(m(!0),h($,null,S(i.value.problems,u=>(m(),h("li",{key:u.line},[c("span",xe,"line "+b(u.line),1),c("code",null,b(u.text),1),c("span",Ee,b(u.reason),1)]))),128))]),r[10]||(r[10]=c("p",null," They are not imported. Fix them and paste again, or apply what parsed and add the rest by hand. ",-1))])):w("",!0),c("div",Ce,[e.replace?(m(),h("button",{key:0,type:"button",class:"primary",disabled:!d.value.length||e.busy,title:d.value.length?"Discard the current list and use exactly what was pasted":"Nothing parsed to apply",onClick:r[4]||(r[4]=u=>_("replace"))}," Replace all ",8,Ve)):w("",!0),c("button",{type:"button",class:H(e.replace?"":"primary"),disabled:!d.value.length||e.busy,title:d.value.length?"Overwrite matching names, keep the rest, add what is new":"Nothing parsed to apply",onClick:r[5]||(r[5]=u=>_("merge"))}," Merge ",10,Ae),c("button",{type:"button",class:"ghost",onClick:r[6]||(r[6]=u=>n.value=!1)},"Cancel")])],64)):w("",!0)])):w("",!0)]))}}),Oe=G(Se,[["__scopeId","data-v-0ae3954c"]]);export{Oe as V,Le as m};
|
|
15
|
+
//# sourceMappingURL=VarIO-BFBaI8oU.js.map
|