@samyx/preview-stacks-ui 0.39.1 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/dist/assets/{ActionButton-OewKsFrK.js → ActionButton-BaGfzZKv.js} +2 -2
  2. package/dist/assets/{ActionButton-OewKsFrK.js.map → ActionButton-BaGfzZKv.js.map} +1 -1
  3. package/dist/assets/{AxesTab-DSzgRhgG.js → AxesTab-8px21p5I.js} +2 -2
  4. package/dist/assets/{AxesTab-DSzgRhgG.js.map → AxesTab-8px21p5I.js.map} +1 -1
  5. package/dist/assets/{ConfigTab-DhtUF0Dh.js → ConfigTab-XMpNHvgd.js} +2 -2
  6. package/dist/assets/{ConfigTab-DhtUF0Dh.js.map → ConfigTab-XMpNHvgd.js.map} +1 -1
  7. package/dist/assets/{ConfigView-wXO4j8H7.js → ConfigView-Ed9sCukp.js} +2 -2
  8. package/dist/assets/{ConfigView-wXO4j8H7.js.map → ConfigView-Ed9sCukp.js.map} +1 -1
  9. package/dist/assets/{ConflictNote.vue_vue_type_script_setup_true_lang-B2fgJAmK.js → ConflictNote.vue_vue_type_script_setup_true_lang-B4a9DxS8.js} +2 -2
  10. package/dist/assets/{ConflictNote.vue_vue_type_script_setup_true_lang-B2fgJAmK.js.map → ConflictNote.vue_vue_type_script_setup_true_lang-B4a9DxS8.js.map} +1 -1
  11. package/dist/assets/ControlView-B-gr_xQ3.css +1 -0
  12. package/dist/assets/ControlView-C4R1joVF.js +2 -0
  13. package/dist/assets/ControlView-C4R1joVF.js.map +1 -0
  14. package/dist/assets/{DangerTab-D2dFrj4t.js → DangerTab-CMIMyWEv.js} +2 -2
  15. package/dist/assets/{DangerTab-D2dFrj4t.js.map → DangerTab-CMIMyWEv.js.map} +1 -1
  16. package/dist/assets/{DashboardView-FpEFhaCl.js → DashboardView-gTjtsiF-.js} +2 -2
  17. package/dist/assets/{DashboardView-FpEFhaCl.js.map → DashboardView-gTjtsiF-.js.map} +1 -1
  18. package/dist/assets/{DeploymentDetailView-B1dKUzvp.js → DeploymentDetailView-Dg-9p6_v.js} +2 -2
  19. package/dist/assets/{DeploymentDetailView-B1dKUzvp.js.map → DeploymentDetailView-Dg-9p6_v.js.map} +1 -1
  20. package/dist/assets/{DeploymentsView-_V93UC6F.js → DeploymentsView-CNzMaxE7.js} +2 -2
  21. package/dist/assets/{DeploymentsView-_V93UC6F.js.map → DeploymentsView-CNzMaxE7.js.map} +1 -1
  22. package/dist/assets/{EquivalentCommand-D-mkbReY.js → EquivalentCommand-CAhOGrAR.js} +2 -2
  23. package/dist/assets/{EquivalentCommand-D-mkbReY.js.map → EquivalentCommand-CAhOGrAR.js.map} +1 -1
  24. package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js → ErrorNote.vue_vue_type_script_setup_true_lang-DU01Dqor.js} +2 -2
  25. package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-FEOV8vdM.js.map → ErrorNote.vue_vue_type_script_setup_true_lang-DU01Dqor.js.map} +1 -1
  26. package/dist/assets/{JobDetailView-RD5MxdA7.js → JobDetailView-ChKP7xeI.js} +2 -2
  27. package/dist/assets/{JobDetailView-RD5MxdA7.js.map → JobDetailView-ChKP7xeI.js.map} +1 -1
  28. package/dist/assets/{JobsView-BJrH9ViP.js → JobsView-DndjCwKL.js} +2 -2
  29. package/dist/assets/{JobsView-BJrH9ViP.js.map → JobsView-DndjCwKL.js.map} +1 -1
  30. package/dist/assets/{LogViewer.vue_vue_type_script_setup_true_lang-BRSt9Exf.js → LogViewer.vue_vue_type_script_setup_true_lang-D2yEiI2n.js} +2 -2
  31. package/dist/assets/{LogViewer.vue_vue_type_script_setup_true_lang-BRSt9Exf.js.map → LogViewer.vue_vue_type_script_setup_true_lang-D2yEiI2n.js.map} +1 -1
  32. package/dist/assets/LoginView-B9aLjuvQ.css +1 -0
  33. package/dist/assets/LoginView-C2Yny6iz.js +2 -0
  34. package/dist/assets/LoginView-C2Yny6iz.js.map +1 -0
  35. package/dist/assets/{LogsTab--Q-KHeWj.js → LogsTab-B6R6j6Uv.js} +2 -2
  36. package/dist/assets/{LogsTab--Q-KHeWj.js.map → LogsTab-B6R6j6Uv.js.map} +1 -1
  37. package/dist/assets/{NotFoundView-1lI66Xt-.js → NotFoundView-CvHMcInC.js} +2 -2
  38. package/dist/assets/{NotFoundView-1lI66Xt-.js.map → NotFoundView-CvHMcInC.js.map} +1 -1
  39. package/dist/assets/{NotifiersView-BfWfRzg4.js → NotifiersView-9rwo6wXV.js} +2 -2
  40. package/dist/assets/{NotifiersView-BfWfRzg4.js.map → NotifiersView-9rwo6wXV.js.map} +1 -1
  41. package/dist/assets/{OverviewTab-P3htOnPs.js → OverviewTab-BO8-8pTB.js} +2 -2
  42. package/dist/assets/{OverviewTab-P3htOnPs.js.map → OverviewTab-BO8-8pTB.js.map} +1 -1
  43. package/dist/assets/{PublicLogsView-DcDQMivg.js → PublicLogsView-BA7ao_jC.js} +2 -2
  44. package/dist/assets/{PublicLogsView-DcDQMivg.js.map → PublicLogsView-BA7ao_jC.js.map} +1 -1
  45. package/dist/assets/{RefreshButton.vue_vue_type_script_setup_true_lang-CQvD4Out.js → RefreshButton.vue_vue_type_script_setup_true_lang-Dk-lSii3.js} +2 -2
  46. package/dist/assets/{RefreshButton.vue_vue_type_script_setup_true_lang-CQvD4Out.js.map → RefreshButton.vue_vue_type_script_setup_true_lang-Dk-lSii3.js.map} +1 -1
  47. package/dist/assets/{RegistriesView-9hPjVsOW.js → RegistriesView-y9nKbmQz.js} +2 -2
  48. package/dist/assets/{RegistriesView-9hPjVsOW.js.map → RegistriesView-y9nKbmQz.js.map} +1 -1
  49. package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-BxIqRu_b.js → RelativeTime.vue_vue_type_script_setup_true_lang-B1_l0Vtf.js} +2 -2
  50. package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-BxIqRu_b.js.map → RelativeTime.vue_vue_type_script_setup_true_lang-B1_l0Vtf.js.map} +1 -1
  51. package/dist/assets/{RequiresTab--C-3QPQB.js → RequiresTab-DJZagYBh.js} +2 -2
  52. package/dist/assets/{RequiresTab--C-3QPQB.js.map → RequiresTab-DJZagYBh.js.map} +1 -1
  53. package/dist/assets/{RouteTarget.vue_vue_type_script_setup_true_lang-D3FLq6Mt.js → RouteTarget.vue_vue_type_script_setup_true_lang-Ct40NR9a.js} +2 -2
  54. package/dist/assets/{RouteTarget.vue_vue_type_script_setup_true_lang-D3FLq6Mt.js.map → RouteTarget.vue_vue_type_script_setup_true_lang-Ct40NR9a.js.map} +1 -1
  55. package/dist/assets/{RoutingView-CC8xPrcT.js → RoutingView-DzE98-qV.js} +2 -2
  56. package/dist/assets/{RoutingView-CC8xPrcT.js.map → RoutingView-DzE98-qV.js.map} +1 -1
  57. package/dist/assets/{RuntimeTab-Do1xfTp8.js → RuntimeTab-Cp6CxBJG.js} +2 -2
  58. package/dist/assets/{RuntimeTab-Do1xfTp8.js.map → RuntimeTab-Cp6CxBJG.js.map} +1 -1
  59. package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js → SelectMenu.vue_vue_type_script_setup_true_lang-PIaLSNsk.js} +2 -2
  60. package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-CMLYr7A7.js.map → SelectMenu.vue_vue_type_script_setup_true_lang-PIaLSNsk.js.map} +1 -1
  61. package/dist/assets/{SettingsView-Dxbl6HZA.js → SettingsView-iWRluEj5.js} +2 -2
  62. package/dist/assets/{SettingsView-Dxbl6HZA.js.map → SettingsView-iWRluEj5.js.map} +1 -1
  63. package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-D4pkKZAr.js → SkeletonList.vue_vue_type_script_setup_true_lang-7pKEuaEM.js} +2 -2
  64. package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-D4pkKZAr.js.map → SkeletonList.vue_vue_type_script_setup_true_lang-7pKEuaEM.js.map} +1 -1
  65. package/dist/assets/{SpecDetailView-DyFAAMfr.js → SpecDetailView-Ca4WXLH1.js} +2 -2
  66. package/dist/assets/{SpecDetailView-DyFAAMfr.js.map → SpecDetailView-Ca4WXLH1.js.map} +1 -1
  67. package/dist/assets/{SpecsView-CfV1_GDx.js → SpecsView-DN6EIbcC.js} +2 -2
  68. package/dist/assets/{SpecsView-CfV1_GDx.js.map → SpecsView-DN6EIbcC.js.map} +1 -1
  69. package/dist/assets/{SsoMark.vue_vue_type_script_setup_true_lang-BU6ThRhq.js → SsoMark.vue_vue_type_script_setup_true_lang-rMP20d_u.js} +2 -2
  70. package/dist/assets/{SsoMark.vue_vue_type_script_setup_true_lang-BU6ThRhq.js.map → SsoMark.vue_vue_type_script_setup_true_lang-rMP20d_u.js.map} +1 -1
  71. package/dist/assets/{SsoView-CxFylJ9d.js → SsoView-wGpab5Q8.js} +2 -2
  72. package/dist/assets/{SsoView-CxFylJ9d.js.map → SsoView-wGpab5Q8.js.map} +1 -1
  73. package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-CfjzlWSc.js → StateBadge.vue_vue_type_script_setup_true_lang-aK93ZMhG.js} +2 -2
  74. package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-CfjzlWSc.js.map → StateBadge.vue_vue_type_script_setup_true_lang-aK93ZMhG.js.map} +1 -1
  75. package/dist/assets/{SubmitView-BcAcTlwC.js → SubmitView-BTK9zNDd.js} +2 -2
  76. package/dist/assets/{SubmitView-BcAcTlwC.js.map → SubmitView-BTK9zNDd.js.map} +1 -1
  77. package/dist/assets/SwarmView-DXLQlbD0.css +1 -0
  78. package/dist/assets/SwarmView-m2XvvAfR.js +2 -0
  79. package/dist/assets/SwarmView-m2XvvAfR.js.map +1 -0
  80. package/dist/assets/{TerminalTab-DF8cSjcg.js → TerminalTab-CFKfVx8D.js} +2 -2
  81. package/dist/assets/{TerminalTab-DF8cSjcg.js.map → TerminalTab-CFKfVx8D.js.map} +1 -1
  82. package/dist/assets/{UsersView-CbPtmnP6.js → UsersView-BnN_hvh2.js} +2 -2
  83. package/dist/assets/{UsersView-CbPtmnP6.js.map → UsersView-BnN_hvh2.js.map} +1 -1
  84. package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-BNQTEru_.js → VarEditor.vue_vue_type_script_setup_true_lang-Dg_ORvZN.js} +2 -2
  85. package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-BNQTEru_.js.map → VarEditor.vue_vue_type_script_setup_true_lang-Dg_ORvZN.js.map} +1 -1
  86. package/dist/assets/{VarIO-BFBaI8oU.js → VarIO-CNugbzeb.js} +2 -2
  87. package/dist/assets/{VarIO-BFBaI8oU.js.map → VarIO-CNugbzeb.js.map} +1 -1
  88. package/dist/assets/{VariablesView-BEcI8Bi0.js → VariablesView-qrqDNseI.js} +2 -2
  89. package/dist/assets/{VariablesView-BEcI8Bi0.js.map → VariablesView-qrqDNseI.js.map} +1 -1
  90. package/dist/assets/{index-D_Ks6aQi.js → index-DlhYSSKq.js} +25 -20
  91. package/dist/assets/index-DlhYSSKq.js.map +1 -0
  92. package/dist/assets/{useDeployment-DFuBcVnK.js → useDeployment-BJuUhf9K.js} +2 -2
  93. package/dist/assets/{useDeployment-DFuBcVnK.js.map → useDeployment-BJuUhf9K.js.map} +1 -1
  94. package/dist/index.html +1 -1
  95. package/package.json +1 -1
  96. package/dist/assets/ControlView-ninZ6BIV.js +0 -2
  97. package/dist/assets/ControlView-ninZ6BIV.js.map +0 -1
  98. package/dist/assets/LoginView-wAfA0Ozk.css +0 -1
  99. package/dist/assets/LoginView-zWd9FvKS.js +0 -2
  100. package/dist/assets/LoginView-zWd9FvKS.js.map +0 -1
  101. package/dist/assets/SwarmView-BHtmW4QF.js +0 -2
  102. package/dist/assets/SwarmView-BHtmW4QF.js.map +0 -1
  103. package/dist/assets/index-D_Ks6aQi.js.map +0 -1
@@ -1 +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&#10;kind: isolated&#10;stack: pr-${PR}&#10;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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"}
1
+ {"version":3,"file":"SubmitView-BTK9zNDd.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&#10;kind: isolated&#10;stack: pr-${PR}&#10;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 @@
1
+ @container (max-width: 460px){table.cards td.name[data-v-16755678]{flex-wrap:wrap;justify-content:flex-end;row-gap:var(--s1)}table.cards td.name[data-v-16755678]:before{margin-right:auto}}
@@ -0,0 +1,2 @@
1
+ import{d as P,x as U,Q as E,c as t,a as e,e as b,z as k,f as r,n as f,t as n,F as y,r as V,h as R,i as d,j as H,a6 as $,o as s,D as M,E as D,u as _,M as w,p as N,C as O}from"./index-DlhYSSKq.js";import{A as q}from"./ActionButton-BaGfzZKv.js";import{_ as A}from"./ErrorNote.vue_vue_type_script_setup_true_lang-DU01Dqor.js";import{_ as J}from"./RefreshButton.vue_vue_type_script_setup_true_lang-Dk-lSii3.js";import{_ as z}from"./SelectMenu.vue_vue_type_script_setup_true_lang-PIaLSNsk.js";import{_ as L}from"./SkeletonList.vue_vue_type_script_setup_true_lang-7pKEuaEM.js";import{_ as Q}from"./_plugin-vue_export-helper-DlAUqK2U.js";const G={class:"page-head"},K={key:2,class:"panel"},W={class:"phead"},X={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},Y={class:"mono"},Z={key:0,class:"banner warn"},ee={key:1,class:"banner plain"},ae={key:0,class:"banner warn"},oe={class:"mono"},se={key:1,role:"table",class:"cards"},te={role:"rowgroup",class:"stagger"},le={role:"cell",class:"name","data-label":"hostname"},ne={key:0,class:"badge info",title:"the node this control plane runs on"},re={key:1,class:"badge warn"},ie={role:"cell","data-label":"role"},de={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},ue={role:"cell","data-label":"status"},me={role:"cell","data-label":"availability"},ce={role:"cell","data-label":"engine",class:"mono"},ve={role:"cell","data-label":"id",class:"mono mute"},pe={key:2,class:"mute"},be={key:3,class:"banner warn"},fe={class:"code",style:{"white-space":"pre-wrap","word-break":"break-all"}},he={key:3,class:"panel"},ke={class:"kvlist",style:{"margin-bottom":"var(--s4)"}},ye={class:"k mono"},_e={class:"v mute"},we={class:"row",style:{"flex-wrap":"wrap",gap:"var(--s3)"}},ge={class:"field inline"},xe={key:0,class:"field inline"},Ce={class:"mute",style:{"font-size":"var(--t-sm)"}},je={key:1,class:"banner leaked",style:{"margin-top":"var(--s3)"}},Se={class:"code",style:{"white-space":"pre-wrap","word-break":"break-all","max-height":"50vh",overflow:"auto"}},Te={class:"row"},Ve=P({__name:"SwarmView",setup($e){const l=d(null),v=d(""),g=d(!1);async function x(){const i=await N.get("/api/swarm");if(g.value=!0,!i.ok){v.value=O(i,"read the swarm");return}v.value="",l.value={...i.body,nodes:i.body.nodes??[],ports:i.body.ports??[]}}U(x,1e4);const C=H(()=>{var i;return((i=l.value)==null?void 0:i.nodes.filter(a=>a.lokiPlugin===!1))??[]}),m=d("command"),h=d("ubuntu"),u=d(""),p=d(!1),c=d(""),j=[{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 B(){p.value=!0,c.value="",u.value="";const i=`format=${m.value}${m.value==="cloud-config"?`&distro=${encodeURIComponent(h.value)}`:""}`,a=await N.getText(`/api/swarm/join?${i}`);if(p.value=!1,a.status===403){c.value="The join token is admin-only.";return}if(a.status===409){c.value="This host is not a swarm manager.";return}if(!a.ok){c.value=a.text||`HTTP ${a.status}`;return}u.value=a.text}async function F(){try{await navigator.clipboard.writeText(u.value),$("ok","Copied.")}catch{$("error","Copy failed — select it and copy by hand.")}}return E(()=>{u.value=""}),(i,a)=>{var S,T;return s(),t("div",null,[e("div",G,[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(J,{run:x})]),v.value?(s(),k(A,{key:0,text:v.value,title:"Could not read the swarm."},null,8,["text"])):r("",!0),!g.value&&!v.value?(s(),k(L,{key:1,rows:4})):l.value?(s(),t("section",K,[e("div",W,[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?(s(),t("span",X,[a[5]||(a[5]=f(" manager ",-1)),e("span",Y,n(l.value.managerAddr),1)])):r("",!0)]),l.value.reachable?l.value.active?(s(),t(y,{key:2},[l.value.error?(s(),t("div",ae,[a[10]||(a[10]=e("b",null,"Docker reported a problem.",-1)),e("p",oe,n(l.value.error),1)])):r("",!0),l.value.nodes.length?(s(),t("table",se,[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",te,[(s(!0),t(y,null,V(l.value.nodes,(o,I)=>(s(),t("tr",{key:o.id,role:"row",style:M({"--i":I})},[e("td",le,[f(n(o.hostname||"—")+" ",1),o.self?(s(),t("span",ne,"This node")):r("",!0),o.lokiPlugin===!1?(s(),t("span",re,"No loki plugin")):r("",!0)]),e("td",ie,[e("span",{class:D(["badge",o.role==="manager"?"isolated":""])},n(_(w)(o.role)),3),o.managerStatus?(s(),t("span",de,n(o.managerStatus),1)):r("",!0)]),e("td",ue,[e("span",{class:D(o.status==="ready"?"s-ok":"s-failed")},n(_(w)(o.status)),3)]),e("td",me,n(_(w)(o.availability||"unknown")),1),e("td",ce,n(o.engineVersion||"—"),1),e("td",ve,n(o.id.slice(0,12)),1)],4))),128))])])):(s(),t("p",pe,"Docker lists no nodes.")),C.value.length?(s(),t("div",be,[e("b",null,"No loki plugin on "+n(C.value.map(o=>o.hostname||o.id.slice(0,12)).join(", "))+".",1),e("pre",fe,n(l.value.lokiPluginInstall),1)])):r("",!0)],64)):(s(),t("div",ee,[a[9]||(a[9]=e("b",null,"Not a swarm manager.",-1)),e("p",null,n(l.value.note),1)])):(s(),t("div",Z,[...a[8]||(a[8]=[e("b",null,"Docker did not answer — this is not the same as no swarm.",-1)])]))])):r("",!0),(S=l.value)!=null&&S.active?(s(),t("section",he,[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",ke,[(s(!0),t(y,null,V(l.value.ports,o=>(s(),t("li",{key:o.port},[e("span",ye,n(o.port),1),e("span",_e,n(o.why),1)]))),128))]),e("div",we,[e("div",ge,[a[12]||(a[12]=e("label",{for:"join-format"},"Format",-1)),b(z,{id:"join-format","model-value":m.value,label:"Join format",options:j.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"?(s(),t("div",xe,[a[13]||(a[13]=e("label",{for:"join-distro"},"Distro",-1)),b(z,{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(q,{variant:"primary",pending:p.value,disabled:p.value,onClick:B},{default:R(()=>[...a[14]||(a[14]=[f(" Reveal ",-1)])]),_:1},8,["pending","disabled"]),e("span",Ce,n((T=j.find(o=>o.value===m.value))==null?void 0:T.blurb),1)]),c.value?(s(),k(A,{key:0,text:c.value,title:"Nothing was revealed."},null,8,["text"])):r("",!0),u.value?(s(),t("div",je,[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",Se,n(u.value),1),e("div",Te,[e("button",{onClick:F},"Copy"),e("button",{onClick:a[2]||(a[2]=o=>u.value="")},"Hide")])])):r("",!0)])):r("",!0)])}}}),Pe=Q(Ve,[["__scopeId","data-v-16755678"]]);export{Pe as default};
2
+ //# sourceMappingURL=SwarmView-m2XvvAfR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SwarmView-m2XvvAfR.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 * WITH LOKI LOGGING ON, a node without the loki log plugin is flagged and the install line shown.\n * Swarm keeps logged services off that node, and nothing here can reach a worker to install it — the\n * line is run there by hand. `lokiPlugin: null` is \"not checked\" (logging off), never \"missing\".\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 { computed, 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// Only `false` is missing; null is \"not checked\".\nconst unplugged = computed(() => info.value?.nodes.filter((n) => n.lokiPlugin === false) ?? []);\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 <span v-if=\"n.lokiPlugin === false\" class=\"badge warn\">No loki plugin</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 <div v-if=\"unplugged.length\" class=\"banner warn\">\n <b>No loki plugin on {{ unplugged.map((n) => n.hostname || n.id.slice(0, 12)).join(', ') }}.</b>\n <pre class=\"code\" style=\"white-space: pre-wrap; word-break: break-all\">{{ info.lokiPluginInstall }}</pre>\n </div>\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\n<style scoped>\n/* Card mode: badges wrap under the hostname instead of crushing it or pushing the card wide. */\n@container (max-width: 460px) {\n table.cards td.name {\n flex-wrap: wrap;\n justify-content: flex-end;\n row-gap: var(--s1);\n }\n table.cards td.name::before {\n margin-right: auto;\n }\n}\n</style>\n"],"names":["info","ref","error","loaded","load","r","api","problem","usePolling","unplugged","computed","_a","n","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","i","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_normalizeClass","_unref","sentence","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_7","_hoisted_6","_hoisted_24","_hoisted_25","_renderList","p","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","SelectMenu","f","v","_hoisted_30","$event","d","ActionButton","_hoisted_31","_b","_hoisted_32","_hoisted_33","_hoisted_34"],"mappings":"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"}
@@ -1,3 +1,3 @@
1
1
  const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/xterm-D6Mng-DB.js","assets/_commonjsHelpers-CqkleIqs.js","assets/addon-fit-CevVMrUA.js","assets/xterm-DYP7pi_n.css"])))=>i.map(i=>d[i]);
2
- import{d as 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
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-DlhYSSKq.js";import{_ as Y}from"./ErrorNote.vue_vue_type_script_setup_true_lang-DU01Dqor.js";import{_ as A}from"./SelectMenu.vue_vue_type_script_setup_true_lang-PIaLSNsk.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-CFKfVx8D.js.map
@@ -1 +1 @@
1
- {"version":3,"mappings":"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","names":["route","useRoute","id","computed","SHELLS","rt","ref","loadError","container","shell","state","closeReason","host","term","shallowRef","fit","socket","observer","containerOptions","running","c","_a","loadContainers","r","api","problem","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"}
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-CFKfVx8D.js"}
@@ -1,2 +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
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-DlhYSSKq.js";import{A as F}from"./ActionButton-BaGfzZKv.js";import{E as Y}from"./EquivalentCommand-CAhOGrAR.js";import{_ as ce}from"./ErrorNote.vue_vue_type_script_setup_true_lang-DU01Dqor.js";import{_ as z}from"./RelativeTime.vue_vue_type_script_setup_true_lang-B1_l0Vtf.js";import{_ as H}from"./SkeletonList.vue_vue_type_script_setup_true_lang-7pKEuaEM.js";import{_ as pe}from"./RefreshButton.vue_vue_type_script_setup_true_lang-Dk-lSii3.js";import{_ as W}from"./SelectMenu.vue_vue_type_script_setup_true_lang-PIaLSNsk.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-BnN_hvh2.js.map