@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.
- package/dist/assets/{ActionButton-OewKsFrK.js → ActionButton-BaGfzZKv.js} +2 -2
- package/dist/assets/{ActionButton-OewKsFrK.js.map → ActionButton-BaGfzZKv.js.map} +1 -1
- package/dist/assets/{AxesTab-DSzgRhgG.js → AxesTab-8px21p5I.js} +2 -2
- package/dist/assets/{AxesTab-DSzgRhgG.js.map → AxesTab-8px21p5I.js.map} +1 -1
- package/dist/assets/{ConfigTab-DhtUF0Dh.js → ConfigTab-XMpNHvgd.js} +2 -2
- package/dist/assets/{ConfigTab-DhtUF0Dh.js.map → ConfigTab-XMpNHvgd.js.map} +1 -1
- package/dist/assets/{ConfigView-wXO4j8H7.js → ConfigView-Ed9sCukp.js} +2 -2
- package/dist/assets/{ConfigView-wXO4j8H7.js.map → ConfigView-Ed9sCukp.js.map} +1 -1
- 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
- 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
- package/dist/assets/ControlView-B-gr_xQ3.css +1 -0
- package/dist/assets/ControlView-C4R1joVF.js +2 -0
- package/dist/assets/ControlView-C4R1joVF.js.map +1 -0
- package/dist/assets/{DangerTab-D2dFrj4t.js → DangerTab-CMIMyWEv.js} +2 -2
- package/dist/assets/{DangerTab-D2dFrj4t.js.map → DangerTab-CMIMyWEv.js.map} +1 -1
- package/dist/assets/{DashboardView-FpEFhaCl.js → DashboardView-gTjtsiF-.js} +2 -2
- package/dist/assets/{DashboardView-FpEFhaCl.js.map → DashboardView-gTjtsiF-.js.map} +1 -1
- package/dist/assets/{DeploymentDetailView-B1dKUzvp.js → DeploymentDetailView-Dg-9p6_v.js} +2 -2
- package/dist/assets/{DeploymentDetailView-B1dKUzvp.js.map → DeploymentDetailView-Dg-9p6_v.js.map} +1 -1
- package/dist/assets/{DeploymentsView-_V93UC6F.js → DeploymentsView-CNzMaxE7.js} +2 -2
- package/dist/assets/{DeploymentsView-_V93UC6F.js.map → DeploymentsView-CNzMaxE7.js.map} +1 -1
- package/dist/assets/{EquivalentCommand-D-mkbReY.js → EquivalentCommand-CAhOGrAR.js} +2 -2
- package/dist/assets/{EquivalentCommand-D-mkbReY.js.map → EquivalentCommand-CAhOGrAR.js.map} +1 -1
- 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
- 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
- package/dist/assets/{JobDetailView-RD5MxdA7.js → JobDetailView-ChKP7xeI.js} +2 -2
- package/dist/assets/{JobDetailView-RD5MxdA7.js.map → JobDetailView-ChKP7xeI.js.map} +1 -1
- package/dist/assets/{JobsView-BJrH9ViP.js → JobsView-DndjCwKL.js} +2 -2
- package/dist/assets/{JobsView-BJrH9ViP.js.map → JobsView-DndjCwKL.js.map} +1 -1
- 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
- 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
- package/dist/assets/LoginView-B9aLjuvQ.css +1 -0
- package/dist/assets/LoginView-C2Yny6iz.js +2 -0
- package/dist/assets/LoginView-C2Yny6iz.js.map +1 -0
- package/dist/assets/{LogsTab--Q-KHeWj.js → LogsTab-B6R6j6Uv.js} +2 -2
- package/dist/assets/{LogsTab--Q-KHeWj.js.map → LogsTab-B6R6j6Uv.js.map} +1 -1
- package/dist/assets/{NotFoundView-1lI66Xt-.js → NotFoundView-CvHMcInC.js} +2 -2
- package/dist/assets/{NotFoundView-1lI66Xt-.js.map → NotFoundView-CvHMcInC.js.map} +1 -1
- package/dist/assets/{NotifiersView-BfWfRzg4.js → NotifiersView-9rwo6wXV.js} +2 -2
- package/dist/assets/{NotifiersView-BfWfRzg4.js.map → NotifiersView-9rwo6wXV.js.map} +1 -1
- package/dist/assets/{OverviewTab-P3htOnPs.js → OverviewTab-BO8-8pTB.js} +2 -2
- package/dist/assets/{OverviewTab-P3htOnPs.js.map → OverviewTab-BO8-8pTB.js.map} +1 -1
- package/dist/assets/{PublicLogsView-DcDQMivg.js → PublicLogsView-BA7ao_jC.js} +2 -2
- package/dist/assets/{PublicLogsView-DcDQMivg.js.map → PublicLogsView-BA7ao_jC.js.map} +1 -1
- 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
- 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
- package/dist/assets/{RegistriesView-9hPjVsOW.js → RegistriesView-y9nKbmQz.js} +2 -2
- package/dist/assets/{RegistriesView-9hPjVsOW.js.map → RegistriesView-y9nKbmQz.js.map} +1 -1
- 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
- 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
- package/dist/assets/{RequiresTab--C-3QPQB.js → RequiresTab-DJZagYBh.js} +2 -2
- package/dist/assets/{RequiresTab--C-3QPQB.js.map → RequiresTab-DJZagYBh.js.map} +1 -1
- 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
- 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
- package/dist/assets/{RoutingView-CC8xPrcT.js → RoutingView-DzE98-qV.js} +2 -2
- package/dist/assets/{RoutingView-CC8xPrcT.js.map → RoutingView-DzE98-qV.js.map} +1 -1
- package/dist/assets/{RuntimeTab-Do1xfTp8.js → RuntimeTab-Cp6CxBJG.js} +2 -2
- package/dist/assets/{RuntimeTab-Do1xfTp8.js.map → RuntimeTab-Cp6CxBJG.js.map} +1 -1
- 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
- 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
- package/dist/assets/{SettingsView-Dxbl6HZA.js → SettingsView-iWRluEj5.js} +2 -2
- package/dist/assets/{SettingsView-Dxbl6HZA.js.map → SettingsView-iWRluEj5.js.map} +1 -1
- 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
- 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
- package/dist/assets/{SpecDetailView-DyFAAMfr.js → SpecDetailView-Ca4WXLH1.js} +2 -2
- package/dist/assets/{SpecDetailView-DyFAAMfr.js.map → SpecDetailView-Ca4WXLH1.js.map} +1 -1
- package/dist/assets/{SpecsView-CfV1_GDx.js → SpecsView-DN6EIbcC.js} +2 -2
- package/dist/assets/{SpecsView-CfV1_GDx.js.map → SpecsView-DN6EIbcC.js.map} +1 -1
- 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
- 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
- package/dist/assets/{SsoView-CxFylJ9d.js → SsoView-wGpab5Q8.js} +2 -2
- package/dist/assets/{SsoView-CxFylJ9d.js.map → SsoView-wGpab5Q8.js.map} +1 -1
- 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
- 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
- package/dist/assets/{SubmitView-BcAcTlwC.js → SubmitView-BTK9zNDd.js} +2 -2
- package/dist/assets/{SubmitView-BcAcTlwC.js.map → SubmitView-BTK9zNDd.js.map} +1 -1
- package/dist/assets/SwarmView-DXLQlbD0.css +1 -0
- package/dist/assets/SwarmView-m2XvvAfR.js +2 -0
- package/dist/assets/SwarmView-m2XvvAfR.js.map +1 -0
- package/dist/assets/{TerminalTab-DF8cSjcg.js → TerminalTab-CFKfVx8D.js} +2 -2
- package/dist/assets/{TerminalTab-DF8cSjcg.js.map → TerminalTab-CFKfVx8D.js.map} +1 -1
- package/dist/assets/{UsersView-CbPtmnP6.js → UsersView-BnN_hvh2.js} +2 -2
- package/dist/assets/{UsersView-CbPtmnP6.js.map → UsersView-BnN_hvh2.js.map} +1 -1
- 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
- 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
- package/dist/assets/{VarIO-BFBaI8oU.js → VarIO-CNugbzeb.js} +2 -2
- package/dist/assets/{VarIO-BFBaI8oU.js.map → VarIO-CNugbzeb.js.map} +1 -1
- package/dist/assets/{VariablesView-BEcI8Bi0.js → VariablesView-qrqDNseI.js} +2 -2
- package/dist/assets/{VariablesView-BEcI8Bi0.js.map → VariablesView-qrqDNseI.js.map} +1 -1
- package/dist/assets/{index-D_Ks6aQi.js → index-DlhYSSKq.js} +25 -20
- package/dist/assets/index-DlhYSSKq.js.map +1 -0
- package/dist/assets/{useDeployment-DFuBcVnK.js → useDeployment-BJuUhf9K.js} +2 -2
- package/dist/assets/{useDeployment-DFuBcVnK.js.map → useDeployment-BJuUhf9K.js.map} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/assets/ControlView-ninZ6BIV.js +0 -2
- package/dist/assets/ControlView-ninZ6BIV.js.map +0 -1
- package/dist/assets/LoginView-wAfA0Ozk.css +0 -1
- package/dist/assets/LoginView-zWd9FvKS.js +0 -2
- package/dist/assets/LoginView-zWd9FvKS.js.map +0 -1
- package/dist/assets/SwarmView-BHtmW4QF.js +0 -2
- package/dist/assets/SwarmView-BHtmW4QF.js.map +0 -1
- package/dist/assets/index-D_Ks6aQi.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UsersView-CbPtmnP6.js","sources":["../../src/views/UsersView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Accounts and personal API tokens.\n *\n * Accounts landed in 0.10.0 with no UI at all, so the only way to add a second operator was a curl\n * call — which is exactly the thing a control plane exists to spare people.\n *\n * TWO AUDIENCES, TWO SECTIONS, and the split is deliberate. **Accounts** are about other people and\n * are shared state; **tokens** are yours alone (the API scopes them to the caller and cannot show\n * anyone else's). Mixing them into one table would imply an admin can manage another person's\n * tokens, which is not true and would be a bad idea if it were.\n *\n * SECRETS ARE SHOWN ONCE, and the page says so in the same breath as showing them. Same discipline\n * as a notifier signing secret and a registry credential: there is no read path on the server, so\n * this is not a policy that could be relaxed later — the value genuinely stops existing.\n *\n * ── ROLES ────────────────────────────────────────────────────────────────────────────────────────\n *\n * The roster is ordinary team information, so ANY signed-in account reads this page. Changing it —\n * adding an account, setting a role, deleting one, setting someone ELSE's password — is admin's,\n * and those controls are simply not rendered for anyone else. Your OWN password stays yours at\n * every role, so that one button is there for everybody, on their own row only.\n *\n * Hiding is UX. `can('admin')` decides what to DRAW; the server's permission table decides what is\n * allowed, and a hidden control that is somehow reached still comes back 403 — which lands in\n * `listError` and says which role the route wanted. That path is not a fallback, it is the actual\n * enforcement, so it stays.\n *\n * THE LAST ADMIN is the one rule the server has that the UI can see coming: it refuses to delete or\n * demote the final admin account. A request that can only fail is worse than a control that says\n * why it cannot be pressed, so that row's role picker and Delete arrive already disabled, carrying\n * the same remedy the server would have answered with — promote another account first.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport { ROLES, type Role, type User, type UsersResponse } from '../api/types';\nimport { authState, can, checkAuth } from '../composables/useAuth';\nimport { sentence } from '../composables/useFormat';\nimport {\n DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogClose,\n} from 'reka-ui';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport RelativeTime from '../components/RelativeTime.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\nimport RefreshButton from '../components/RefreshButton.vue';\nimport SelectMenu from '../components/SelectMenu.vue';\n\ntype Token = { id: number; name: string; createdAt: number; lastUsedAt: number | null };\n\nconst users = ref<User[]>([]);\nconst tokens = ref<Token[]>([]);\nconst loaded = ref(false);\nconst listError = ref('');\n\nconst newUser = ref<{ username: string; password: string; role: Role }>({\n username: '',\n password: '',\n role: 'viewer',\n});\nconst newToken = ref('');\nconst pwFor = ref<User | null>(null);\nconst pwValue = ref('');\n\n/** Held only until dismissed — there is no way to show either of these again. */\nconst revealed = ref<{ what: string; value: string; note: string } | null>(null);\n\nconst canCreateUser = computed(\n () => newUser.value.username.trim().length > 0 && newUser.value.password.length >= 8,\n);\n\n/** One line each, in the picker itself — a role name alone does not say what it costs to grant. */\nconst ROLE_WHAT: Record<Role, string> = {\n viewer: 'reads everything',\n developer: 'deploys and tears down',\n maintainer: 'host configuration',\n admin: 'accounts and sign-on',\n};\nconst roleOptions = ROLES.map((r) => ({ value: r, label: sentence(r), hint: ROLE_WHAT[r] }));\n\nconst adminCount = computed(() => users.value.filter((u) => u.role === 'admin').length);\n/**\n * The last admin cannot be demoted or deleted — the server refuses both, and a host with no admin\n * has nobody who can add one back. Counting ROWS is the same thing the server counts: root holds\n * PSTACK_TOKEN, not an account, and may not exist as a human at all.\n */\nfunction lastAdmin(u: User): boolean {\n return u.role === 'admin' && adminCount.value === 1;\n}\nconst LAST_ADMIN_WHY = 'The only admin — promote another account first.';\n\nasync function load(): Promise<void> {\n const [u, t] = await Promise.all([\n api.get<UsersResponse>('/api/users'),\n api.get<{ tokens: Token[] }>('/api/tokens'),\n ]);\n loaded.value = true;\n if (!u.ok) {\n listError.value = problem(u, 'load the accounts');\n return;\n }\n listError.value = '';\n users.value = u.body.users ?? [];\n // Root (PSTACK_TOKEN) is not a user, so it has no personal tokens — an error here is expected.\n tokens.value = t.ok ? (t.body.tokens ?? []) : [];\n}\nvoid load();\n\nasync function createUser(): Promise<void> {\n // The role is always sent, never left to the server's default: what this form creates should be\n // whatever the form is showing. (An absent role means viewer there, which is the same answer —\n // but the day it is not, this page would be creating something other than what it said.)\n const r = await api.post<{ user: User }>('/api/users', {\n username: newUser.value.username.trim(),\n password: newUser.value.password,\n role: newUser.value.role,\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this account');\n return;\n }\n toast('ok', `Added ${r.body.user.username}.`);\n newUser.value = { username: '', password: '', role: 'viewer' };\n void load();\n}\n\nasync function removeUser(u: User): Promise<void> {\n const r = await api.del(`/api/users/${u.id}`);\n // The server refuses to delete the last account. That is a rule worth surfacing verbatim rather\n // than as \"could not delete\" — it tells the operator what to do instead.\n if (!r.ok) {\n listError.value = problem(r, `delete ${u.username}`);\n return;\n }\n toast('ok', `Deleted ${u.username}.`);\n void load();\n}\n\nasync function changeRole(u: User, role: string): Promise<void> {\n if (role === u.role) return;\n const r = await api.patch(`/api/users/${u.id}`, { role });\n if (!r.ok) {\n listError.value = problem(r, `change the role for ${u.username}`);\n return;\n }\n listError.value = '';\n if (u.id === authState.user?.id) {\n // You just changed your OWN role, and this whole app — the rail, this page's controls, the\n // footer — is drawn from the role it is holding. Re-read it before anything renders again,\n // or the UI keeps offering what the server will now refuse. (The server needs no prompting:\n // every request looks the role up fresh.)\n await checkAuth();\n toast('ok', `You are now a ${role}.`);\n } else {\n toast('ok', `${u.username} is now a ${role}.`);\n }\n void load();\n}\n\nasync function changePassword(): Promise<void> {\n const u = pwFor.value;\n if (!u) return;\n const r = await api.put(`/api/users/${u.id}/password`, { password: pwValue.value });\n if (!r.ok) {\n listError.value = problem(r, `change the password for ${u.username}`);\n return;\n }\n pwFor.value = null;\n pwValue.value = '';\n // If it was YOU, the next request is a 401 — the toast says to sign in again so that reads as an\n // expected step rather than a bug. The dialog carried the full warning before the change.\n toast(\n 'ok',\n u.id === authState.user?.id ? `Changed — sign in again.` : `Password changed.`,\n );\n void load();\n}\n\nasync function createToken(): Promise<void> {\n const r = await api.post<{ token: string; id: number }>('/api/tokens', {\n name: newToken.value.trim(),\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this token');\n return;\n }\n revealed.value = {\n what: `Personal token “${newToken.value.trim()}”`,\n value: r.body.token,\n note: 'Shown once — the server keeps only a hash. It can do anything your account can.',\n };\n newToken.value = '';\n void load();\n}\n\nasync function removeToken(t: Token): Promise<void> {\n const r = await api.del(`/api/tokens/${t.id}`);\n if (!r.ok) {\n listError.value = problem(r, `revoke ${t.name}`);\n return;\n }\n toast('ok', `Revoked ${t.name}.`);\n void load();\n}\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>Users & access</h1>\n <div class=\"sub\">Accounts and personal tokens</div>\n </div>\n <span class=\"grow\" />\n <RefreshButton :run=\"load\" />\n </div>\n\n <ErrorNote v-if=\"listError\" :text=\"listError\" title=\"Could not load the accounts.\" />\n\n <div v-if=\"revealed\" class=\"banner ok\">\n <b>{{ revealed.what }} — copy it now.</b>\n <!-- The one-line interlock: there is no read path on the server, so this really is the only\n time the value exists anywhere but the clipboard. -->\n <p>{{ revealed.note }}</p>\n <pre class=\"code\">{{ revealed.value }}</pre>\n <div class=\"row\">\n <button @click=\"revealed = null\">Dismiss</button>\n </div>\n </div>\n\n <div class=\"grid-2\">\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Accounts</h2>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ users.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"3\" tall />\n <!-- `cards` + `data-label` per cell: below 640px the same markup renders as cards. Without\n it the row's two buttons pushed the table past the viewport and got clipped. -->\n <table v-else-if=\"users.length\" role=\"table\" class=\"cards\">\n <thead role=\"rowgroup\">\n <tr role=\"row\">\n <th role=\"columnheader\">User</th>\n <th role=\"columnheader\">Role</th>\n <th role=\"columnheader\">Added</th>\n <th role=\"columnheader\" aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody role=\"rowgroup\">\n <tr v-for=\"u in users\" :key=\"u.id\" role=\"row\">\n <td role=\"cell\" class=\"name\" data-label=\"user\">\n <b>{{ u.username }}</b>\n <span v-if=\"u.id === authState.user?.id\" class=\"badge info\">you</span>\n </td>\n <!-- An admin gets the picker in place: promotion is a one-step decision about one\n person, and a dialog for a four-option choice is a page-blocking ceremony. Anyone\n else reads the role as text — `sentence()` at render only, never on the value.\n A role this build does not know still shows: it falls through to `placeholder`. -->\n <td role=\"cell\" data-label=\"role\">\n <span\n v-if=\"can('admin')\"\n class=\"role-pick\"\n :title=\"lastAdmin(u) ? LAST_ADMIN_WHY : undefined\"\n >\n <SelectMenu\n :model-value=\"u.role\"\n :label=\"`Role for ${u.username}`\"\n :options=\"roleOptions\"\n :disabled=\"lastAdmin(u)\"\n :placeholder=\"sentence(u.role)\"\n @update:model-value=\"(v) => changeRole(u, v)\"\n />\n </span>\n <span v-else>{{ sentence(u.role) }}</span>\n </td>\n <td role=\"cell\" class=\"dim nowrap\" data-label=\"added\"><RelativeTime :at=\"u.createdAt\" /></td>\n <td role=\"cell\" class=\"right nowrap\" data-label=\"\">\n <!-- Your own password is yours at any role; someone else's is an admin operation. -->\n <button\n v-if=\"u.id === authState.user?.id || can('admin')\"\n class=\"ghost sm\"\n @click=\"pwFor = u; pwValue = ''\"\n >\n Change password\n </button>\n <ActionButton\n v-if=\"can('admin')\"\n class=\"danger sm\"\n :confirm=\"`Delete ${u.username}?`\"\n :disabled=\"lastAdmin(u)\"\n :title=\"lastAdmin(u) ? LAST_ADMIN_WHY : undefined\"\n @run=\"removeUser(u)\"\n >Delete</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No accounts yet.</p>\n\n <form v-if=\"can('admin')\" class=\"stack-form\" @submit.prevent=\"createUser\">\n <h3>Add an account</h3>\n <div class=\"row wrap two-up\">\n <label class=\"field grow\">\n <span>Username</span>\n <input v-model=\"newUser.username\" type=\"text\" autocomplete=\"off\" />\n </label>\n <label class=\"field grow\">\n <span>Password</span>\n <input v-model=\"newUser.password\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n </div>\n <div class=\"field\">\n <label for=\"new-role\">Role</label>\n <SelectMenu\n id=\"new-role\"\n :model-value=\"newUser.role\"\n label=\"Role for the new account\"\n :options=\"roleOptions\"\n @update:model-value=\"(v) => (newUser.role = v as Role)\"\n />\n </div>\n <p class=\"hint\">At least 8 characters.</p>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!canCreateUser\">Add account</button>\n <span class=\"grow\" />\n </div>\n <EquivalentCommand\n what=\"this account creation\"\n method=\"POST\"\n path=\"/api/users\"\n :body=\"{ username: newUser.username || 'alice', password: '…', role: newUser.role }\"\n />\n </form>\n <!-- Not an apology for a missing button: reading the roster is every account's, changing it\n is admin's, and the roster above already names who to ask. -->\n <p v-else class=\"hint\">Only an admin can add accounts or change roles.</p>\n </section>\n\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Your API tokens</h2>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" tall />\n <table v-else-if=\"tokens.length\" role=\"table\" class=\"cards\">\n <thead role=\"rowgroup\">\n <tr role=\"row\">\n <th role=\"columnheader\">Name</th>\n <th role=\"columnheader\">Last used</th>\n <th role=\"columnheader\" aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody role=\"rowgroup\">\n <tr v-for=\"t in tokens\" :key=\"t.id\" role=\"row\">\n <td role=\"cell\" class=\"name\" data-label=\"name\"><b>{{ t.name }}</b></td>\n <td role=\"cell\" class=\"dim nowrap\" data-label=\"last used\">\n <RelativeTime v-if=\"t.lastUsedAt\" :at=\"t.lastUsedAt\" />\n <span v-else class=\"mute\">never</span>\n </td>\n <td role=\"cell\" class=\"right nowrap\" data-label=\"\">\n <ActionButton\n class=\"danger sm\"\n :confirm=\"`Revoke ${t.name}?`\"\n @run=\"removeToken(t)\"\n >Revoke</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <!-- Two different facts, not one empty list: the host token cannot own tokens at all. -->\n <p v-else class=\"hint\">\n {{ authState.root ? 'The host token has no personal tokens.' : 'No tokens yet — name one below.' }}\n </p>\n\n <form v-if=\"!authState.root\" class=\"stack-form\" @submit.prevent=\"createToken\">\n <h3>New token</h3>\n <label class=\"field\">\n <span>Name</span>\n <input v-model=\"newToken\" type=\"text\" placeholder=\"ci-deploy\" autocomplete=\"off\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!newToken.trim()\">Create token</button>\n </div>\n <EquivalentCommand\n what=\"this token creation\"\n method=\"POST\"\n path=\"/api/tokens\"\n :body=\"{ name: newToken || 'ci-deploy' }\"\n />\n </form>\n </section>\n </div>\n\n <!-- Password change: a dialog, because it is a distinct decision about one row. Reka owns\n modality — hand-rolling it declared `aria-modal` without the focus trap it promises. -->\n <DialogRoot :open=\"!!pwFor\" @update:open=\"(v: boolean) => !v && (pwFor = null)\">\n <DialogPortal>\n <DialogOverlay class=\"scrim help-scrim\" />\n <DialogContent v-if=\"pwFor\" class=\"sheet\" aria-label=\"Change password\">\n <DialogTitle as=\"h3\">Change password for {{ pwFor.username }}</DialogTitle>\n <!-- The interlock, one line: this ends every session and revokes every personal token. -->\n <p class=\"hint\">\n Signs {{ pwFor.id === authState.user?.id ? 'you' : 'them' }} out everywhere and revokes\n {{ pwFor.id === authState.user?.id ? 'your' : 'their' }} tokens.\n </p>\n <label class=\"field\">\n <span>New password</span>\n <input v-model=\"pwValue\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" :disabled=\"pwValue.length < 8\" @click=\"changePassword\">\n Change password\n </button>\n <DialogClose class=\"ghost\">Cancel</DialogClose>\n </div>\n </DialogContent>\n </DialogPortal>\n </DialogRoot>\n </div>\n</template>\n\n<style scoped>\n.stack-form {\n margin-top: var(--s5);\n padding-top: var(--s4);\n border-top: 1px solid var(--line-soft);\n display: flex;\n flex-direction: column;\n gap: var(--s3);\n}\n.stack-form h3 {\n font-size: var(--t-sm);\n color: var(--fg-dim);\n}\ntd.right {\n text-align: end;\n}\n/* Two fields share a line where there is room and stack where there is not — at 390px a pair of\n * half-width inputs is two inputs nobody can type into. */\n.two-up > .field {\n flex: 1 1 200px;\n}\ntd.right button {\n margin-inline-start: var(--s2);\n}\n/* The picker sits in a row, not a form: it takes the width its longest label needs and no more,\n * so the Role column does not eat the table. The wrapper exists to carry `title` when the control\n * inside it is disabled — a disabled button does not reliably show one of its own. */\n.role-pick {\n display: inline-block;\n}\n.role-pick :deep(.selm-trigger) {\n width: auto;\n min-width: 150px;\n}\n</style>\n"],"names":["LAST_ADMIN_WHY","users","ref","tokens","loaded","listError","newUser","newToken","pwFor","pwValue","revealed","canCreateUser","computed","ROLE_WHAT","roleOptions","ROLES","r","sentence","adminCount","u","lastAdmin","load","t","api","problem","createUser","toast","removeUser","changeRole","role","_a","authState","checkAuth","changePassword","createToken","removeToken","_createElementBlock","_createElementVNode","_hoisted_1","_createVNode","RefreshButton","_createBlock","ErrorNote","_openBlock","_hoisted_2","_toDisplayString","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_cache","_hoisted_8","_hoisted_9","_hoisted_10","_Fragment","_renderList","_hoisted_11","_unref","_hoisted_12","_hoisted_13","can","SelectMenu","v","_hoisted_15","_hoisted_16","RelativeTime","_hoisted_17","$event","_hoisted_18","ActionButton","_hoisted_19","SkeletonList","_hoisted_20","_hoisted_21","_vModelText","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","EquivalentCommand","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","_hoisted_31","_hoisted_32","_hoisted_33","_hoisted_34","_hoisted_35","_hoisted_36","_hoisted_37","DialogRoot","DialogPortal","DialogOverlay","DialogContent","DialogTitle","_hoisted_38","_hoisted_39","_hoisted_40","_hoisted_41","DialogClose"],"mappings":"+6DA2FMA,EAAiB,sFAvCvB,MAAMC,EAAQC,EAAY,EAAE,EACtBC,EAASD,EAAa,EAAE,EACxBE,EAASF,EAAI,EAAK,EAClBG,EAAYH,EAAI,EAAE,EAElBI,EAAUJ,EAAwD,CACtE,SAAU,GACV,SAAU,GACV,KAAM,QAAA,CACP,EACKK,EAAWL,EAAI,EAAE,EACjBM,EAAQN,EAAiB,IAAI,EAC7BO,EAAUP,EAAI,EAAE,EAGhBQ,EAAWR,EAA0D,IAAI,EAEzES,EAAgBC,EACpB,IAAMN,EAAQ,MAAM,SAAS,KAAA,EAAO,OAAS,GAAKA,EAAQ,MAAM,SAAS,QAAU,CAAA,EAI/EO,EAAkC,CACtC,OAAQ,mBACR,UAAW,yBACX,WAAY,qBACZ,MAAO,sBAAA,EAEHC,EAAcC,GAAM,IAAKC,IAAO,CAAE,MAAOA,EAAG,MAAOC,EAASD,CAAC,EAAG,KAAMH,EAAUG,CAAC,GAAI,EAErFE,EAAaN,EAAS,IAAMX,EAAM,MAAM,OAAQkB,GAAMA,EAAE,OAAS,OAAO,EAAE,MAAM,EAMtF,SAASC,EAAUD,EAAkB,CACnC,OAAOA,EAAE,OAAS,SAAWD,EAAW,QAAU,CACpD,CAGA,eAAeG,GAAsB,CACnC,KAAM,CAACF,EAAGG,CAAC,EAAI,MAAM,QAAQ,IAAI,CAC/BC,EAAI,IAAmB,YAAY,EACnCA,EAAI,IAAyB,aAAa,CAAA,CAC3C,EAED,GADAnB,EAAO,MAAQ,GACX,CAACe,EAAE,GAAI,CACTd,EAAU,MAAQmB,EAAQL,EAAG,mBAAmB,EAChD,MACF,CACAd,EAAU,MAAQ,GAClBJ,EAAM,MAAQkB,EAAE,KAAK,OAAS,CAAA,EAE9BhB,EAAO,MAAQmB,EAAE,GAAMA,EAAE,KAAK,QAAU,CAAA,EAAM,CAAA,CAChD,CACKD,EAAA,EAEL,eAAeI,GAA4B,CAIzC,MAAMT,EAAI,MAAMO,EAAI,KAAqB,aAAc,CACrD,SAAUjB,EAAQ,MAAM,SAAS,KAAA,EACjC,SAAUA,EAAQ,MAAM,SACxB,KAAMA,EAAQ,MAAM,IAAA,CACrB,EACD,GAAI,CAACU,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,qBAAqB,EAClD,MACF,CACAU,EAAM,KAAM,SAASV,EAAE,KAAK,KAAK,QAAQ,GAAG,EAC5CV,EAAQ,MAAQ,CAAE,SAAU,GAAI,SAAU,GAAI,KAAM,QAAA,EAC/Ce,EAAA,CACP,CAEA,eAAeM,EAAWR,EAAwB,CAChD,MAAMH,EAAI,MAAMO,EAAI,IAAI,cAAcJ,EAAE,EAAE,EAAE,EAG5C,GAAI,CAACH,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,UAAUG,EAAE,QAAQ,EAAE,EACnD,MACF,CACAO,EAAM,KAAM,WAAWP,EAAE,QAAQ,GAAG,EAC/BE,EAAA,CACP,CAEA,eAAeO,EAAWT,EAASU,EAA6B,OAC9D,GAAIA,IAASV,EAAE,KAAM,OACrB,MAAMH,EAAI,MAAMO,EAAI,MAAM,cAAcJ,EAAE,EAAE,GAAI,CAAE,KAAAU,EAAM,EACxD,GAAI,CAACb,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,uBAAuBG,EAAE,QAAQ,EAAE,EAChE,MACF,CACAd,EAAU,MAAQ,GACdc,EAAE,OAAOW,EAAAC,EAAU,OAAV,YAAAD,EAAgB,KAK3B,MAAME,GAAA,EACNN,EAAM,KAAM,iBAAiBG,CAAI,GAAG,GAEpCH,EAAM,KAAM,GAAGP,EAAE,QAAQ,aAAaU,CAAI,GAAG,EAE1CR,EAAA,CACP,CAEA,eAAeY,GAAgC,OAC7C,MAAMd,EAAIX,EAAM,MAChB,GAAI,CAACW,EAAG,OACR,MAAMH,EAAI,MAAMO,EAAI,IAAI,cAAcJ,EAAE,EAAE,YAAa,CAAE,SAAUV,EAAQ,KAAA,CAAO,EAClF,GAAI,CAACO,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,2BAA2BG,EAAE,QAAQ,EAAE,EACpE,MACF,CACAX,EAAM,MAAQ,KACdC,EAAQ,MAAQ,GAGhBiB,EACE,KACAP,EAAE,OAAOW,EAAAC,EAAU,OAAV,YAAAD,EAAgB,IAAK,2BAA6B,mBAAA,EAExDT,EAAA,CACP,CAEA,eAAea,IAA6B,CAC1C,MAAMlB,EAAI,MAAMO,EAAI,KAAoC,cAAe,CACrE,KAAMhB,EAAS,MAAM,KAAA,CAAK,CAC3B,EACD,GAAI,CAACS,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,mBAAmB,EAChD,MACF,CACAN,EAAS,MAAQ,CACf,KAAM,mBAAmBH,EAAS,MAAM,MAAM,IAC9C,MAAOS,EAAE,KAAK,MACd,KAAM,iFAAA,EAERT,EAAS,MAAQ,GACZc,EAAA,CACP,CAEA,eAAec,GAAYb,EAAyB,CAClD,MAAMN,EAAI,MAAMO,EAAI,IAAI,eAAeD,EAAE,EAAE,EAAE,EAC7C,GAAI,CAACN,EAAE,GAAI,CACTX,EAAU,MAAQmB,EAAQR,EAAG,UAAUM,EAAE,IAAI,EAAE,EAC/C,MACF,CACAI,EAAM,KAAM,WAAWJ,EAAE,IAAI,GAAG,EAC3BD,EAAA,CACP,mBAIEe,EAuNM,MAAA,KAAA,CAtNJC,EAOM,MAPNC,GAOM,aANJD,EAGM,MAAA,KAAA,CAFJA,EAA2B,UAAvB,gBAAkB,EACtBA,EAAmD,MAAA,CAA9C,MAAM,KAAA,EAAM,8BAA4B,CAAA,mBAE/CA,EAAqB,OAAA,CAAf,MAAM,MAAA,EAAM,KAAA,EAAA,GAClBE,EAA6BC,GAAA,CAAb,IAAKnB,EAAI,CAAA,GAGVhB,EAAA,WAAjBoC,EAAqFC,GAAA,OAAxD,KAAMrC,EAAA,MAAW,MAAM,8BAAA,6BAEzCK,EAAA,OAAXiC,EAAA,EAAAP,EASM,MATNQ,GASM,CARJP,EAAyC,IAAA,KAAAQ,EAAnCnC,EAAA,MAAS,IAAI,EAAG,kBAAe,CAAA,EAGrC2B,EAA0B,IAAA,KAAAQ,EAApBnC,EAAA,MAAS,IAAI,EAAA,CAAA,EACnB2B,EAA4C,MAA5CS,GAA4CD,EAAvBnC,EAAA,MAAS,KAAK,EAAA,CAAA,EACnC2B,EAEM,MAFNU,GAEM,CADJV,EAAiD,SAAA,CAAxC,uBAAO3B,EAAA,MAAQ,KAAA,EAAS,SAAO,CAAA,eAI5C2B,EAqKM,MArKNW,GAqKM,CApKJX,EA4GU,UA5GVY,GA4GU,CA3GRZ,EAIM,MAJNa,GAIM,CAHJC,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAd,EAAiC,KAAA,CAA7B,MAAM,SAAA,EAAU,WAAQ,EAAA,iBAC5BA,EAAqB,OAAA,CAAf,MAAM,MAAA,EAAM,KAAA,EAAA,GAClBA,EAA2E,OAA3Ee,GAA2EP,EAAtB5C,EAAA,MAAM,MAAM,EAAA,CAAA,CAAA,GAG9CG,EAAA,MAGHH,EAAA,MAAM,QAAxB0C,EAAA,EAAAP,EA0DQ,QA1DRiB,GA0DQ,eAzDNhB,EAOQ,QAAA,CAPD,KAAK,YAAU,CACpBA,EAKK,KAAA,CALD,KAAK,OAAK,CACZA,EAAiC,KAAA,CAA7B,KAAK,cAAA,EAAe,MAAI,EAC5BA,EAAiC,KAAA,CAA7B,KAAK,cAAA,EAAe,MAAI,EAC5BA,EAAkC,KAAA,CAA9B,KAAK,cAAA,EAAe,OAAK,EAC7BA,EAA+C,KAAA,CAA3C,KAAK,eAAe,aAAW,SAAA,WAGvCA,EAgDQ,QAhDRiB,GAgDQ,QA/CNlB,EA8CKmB,EAAA,KAAAC,EA9CWvD,EAAA,MAALkB,GAAC,oBAAZiB,EA8CK,KAAA,CA9CmB,IAAKjB,EAAE,GAAI,KAAK,KAAA,GACtCkB,EAGK,KAHLoB,GAGK,CAFHpB,EAAuB,IAAA,KAAAQ,EAAjB1B,EAAE,QAAQ,EAAA,CAAA,EACJA,EAAE,OAAOuC,EAAAA,KAAU,OAAVA,YAAAA,EAAgB,SAArCtB,EAAsE,OAAtEuB,GAA4D,KAAG,cAMjEtB,EAgBK,KAhBLuB,GAgBK,CAdKF,EAAAG,CAAA,EAAG,OAAA,OADXzB,EAaO,OAAA,OAXL,MAAM,YACL,MAAOhB,EAAUD,CAAC,EAAInB,EAAiB,MAAA,GAExCuC,EAOEuB,EAAA,CANC,cAAa3C,EAAE,KACf,MAAK,YAAcA,EAAE,QAAQ,GAC7B,QAASuC,EAAA5C,CAAA,EACT,SAAUM,EAAUD,CAAC,EACrB,YAAauC,EAAAzC,CAAA,EAASE,EAAE,IAAI,EAC5B,sBAAqB4C,GAAMnC,EAAWT,EAAG4C,CAAC,CAAA,oGAG/CpB,EAAA,EAAAP,EAA0C,OAAA4B,GAAAnB,EAA1Ba,EAAAzC,CAAA,EAASE,EAAE,IAAI,CAAA,EAAA,CAAA,EAAA,GAEjCkB,EAA6F,KAA7F4B,GAA6F,CAAvC1B,EAAkC2B,EAAA,CAAnB,GAAI/C,EAAE,SAAA,mBAC3EkB,EAkBK,KAlBL8B,GAkBK,CAfKhD,EAAE,OAAOuC,EAAAA,EAAA3B,CAAA,EAAU,OAAV2B,YAAAA,EAAgB,KAAMA,EAAAG,CAAA,EAAG,OAAA,OAD1CzB,EAMS,SAAA,OAJP,MAAM,WACL,QAAKgC,GAAA,CAAE5D,EAAA,MAAQW,EAAGV,EAAA,MAAO,EAAA,CAAA,EAC3B,oBAED,EAAA4D,EAAA,YAEQX,EAAAG,CAAA,EAAG,OAAA,OADXpB,EAQC6B,EAAA,OANC,MAAM,YACL,QAAO,UAAYnD,EAAE,QAAQ,IAC7B,SAAUC,EAAUD,CAAC,EACrB,MAAOC,EAAUD,CAAC,EAAInB,EAAiB,OACvC,MAAGoE,GAAEzC,EAAWR,CAAC,CAAA,aACjB,IAAM,CAAA,GAAAgC,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAA,GAAN,SAAM,EAAA,CAAA,oFAMjBf,EAA2C,IAA3CmC,GAAuB,kBAAgB,QA9DvC9B,EAA8C+B,EAAA,OAAhB,KAAM,EAAG,KAAA,EAAA,IAgE3Bd,EAAAG,CAAA,EAAG,OAAA,OAAfzB,EAiCO,OAAA,OAjCmB,MAAM,aAAc,WAAgBX,EAAU,CAAA,SAAA,CAAA,CAAA,GACtE0B,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAuB,UAAnB,iBAAc,EAAA,GAClBA,EASM,MATNoC,GASM,CARJpC,EAGQ,QAHRqC,GAGQ,CAFNvB,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAqB,YAAf,WAAQ,EAAA,KACdA,EAAmE,QAAA,CAAnD,sBAAAc,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAiB,GAAA9D,EAAA,MAAQ,SAAQ8D,GAAE,KAAK,OAAO,aAAa,KAAA,aAA3C,CAAAO,EAAArE,EAAA,MAAQ,QAAQ,CAAA,KAElC+B,EAGQ,QAHRuC,GAGQ,CAFNzB,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAqB,YAAf,WAAQ,EAAA,KACdA,EAAgF,QAAA,CAAhE,sBAAAc,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAAiB,GAAA9D,EAAA,MAAQ,SAAQ8D,GAAE,KAAK,WAAW,aAAa,cAAA,aAA/C,CAAAO,EAAArE,EAAA,MAAQ,QAAQ,CAAA,OAGpC+B,EASM,MATNwC,GASM,CARJ1B,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAkC,QAAA,CAA3B,IAAI,UAAA,EAAW,OAAI,EAAA,GAC1BE,EAMEuB,EAAA,CALA,GAAG,WACF,cAAaxD,EAAA,MAAQ,KACtB,MAAM,2BACL,QAASoD,EAAA5C,CAAA,EACT,kCAAqBiD,GAAOzD,QAAQ,KAAOyD,EAAA,sCAGhDZ,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAA0C,IAAA,CAAvC,MAAM,MAAA,EAAO,yBAAsB,EAAA,GACtCA,EAGM,MAHNyC,GAGM,CAFJzC,EAAqF,SAAA,CAA7E,MAAM,UAAU,KAAK,SAAU,UAAW1B,EAAA,KAAA,EAAe,cAAW,EAAAoE,EAAA,gBAC5E1C,EAAqB,OAAA,CAAf,MAAM,QAAM,KAAA,EAAA,EAAA,GAEpBE,EAKEyC,EAAA,CAJA,KAAK,wBACL,OAAO,OACP,KAAK,aACJ,eAAkB1E,EAAA,MAAQ,UAAQ,QAAA,SAAA,IAAA,KAAkCA,EAAA,MAAQ,IAAA,CAAI,8BAKrF8B,EAA0E,IAA1E6C,GAAuB,iDAA+C,EAAA,GAGxE5C,EAqDU,UArDV6C,GAqDU,eApDR7C,EAEM,MAAA,CAFD,MAAM,SAAO,CAChBA,EAAwC,KAAA,CAApC,MAAM,SAAA,EAAU,iBAAe,CAAA,OAGhBjC,EAAA,MACHD,EAAA,MAAO,QAAzBwC,EAAA,EAAAP,EAyBQ,QAzBR+C,GAyBQ,eAxBN9C,EAMQ,QAAA,CAND,KAAK,YAAU,CACpBA,EAIK,KAAA,CAJD,KAAK,OAAK,CACZA,EAAiC,KAAA,CAA7B,KAAK,cAAA,EAAe,MAAI,EAC5BA,EAAsC,KAAA,CAAlC,KAAK,cAAA,EAAe,WAAS,EACjCA,EAA+C,KAAA,CAA3C,KAAK,eAAe,aAAW,SAAA,WAGvCA,EAgBQ,QAhBR+C,GAgBQ,QAfNhD,EAcKmB,EAAA,KAAAC,EAdWrD,EAAA,MAALmB,QAAXc,EAcK,KAAA,CAdoB,IAAKd,EAAE,GAAI,KAAK,KAAA,GACvCe,EAAuE,KAAvEgD,GAAuE,CAAxBhD,EAAmB,IAAA,KAAAQ,EAAbvB,EAAE,IAAI,EAAA,CAAA,CAAA,GAC3De,EAGK,KAHLiD,GAGK,CAFiBhE,EAAE,gBAAtBmB,EAAuDyB,EAAA,OAApB,GAAI5C,EAAE,UAAA,uBACzCc,EAAsC,OAAtCmD,GAA0B,OAAK,EAAA,GAEjClD,EAOK,KAPLmD,GAOK,CANHjD,EAKC+B,EAAA,CAJC,MAAM,YACL,QAAO,UAAYhD,EAAE,IAAI,IACzB,MAAG8C,GAAEjC,GAAYb,CAAC,CAAA,aAClB,IAAM,CAAA,GAAA6B,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAA,GAAN,SAAM,EAAA,CAAA,mDAOjBR,EAAA,EAAAP,EAEI,IAFJqD,GAEI5C,EADCa,EAAA3B,CAAA,EAAU,KAAI,yCAAA,iCAAA,EAAA,CAAA,QA7BnBU,EAA8C+B,EAAA,OAAhB,KAAM,EAAG,KAAA,EAAA,IAgC1Bd,EAAA3B,CAAA,EAAU,mBAAvBK,EAeO,OAAA,OAfsB,MAAM,aAAc,WAAgBF,GAAW,CAAA,SAAA,CAAA,CAAA,GAC1EiB,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAkB,UAAd,YAAS,EAAA,GACbA,EAGQ,QAHRqD,GAGQ,CAFNvC,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAiB,YAAX,OAAI,EAAA,KACVA,EAAmF,QAAA,sCAAnE9B,EAAQ,MAAA6D,GAAE,KAAK,OAAO,YAAY,YAAY,aAAa,KAAA,gBAA3D7D,EAAA,KAAQ,CAAA,KAE1B8B,EAEM,MAFNsD,GAEM,CADJtD,EAAwF,SAAA,CAAhF,MAAM,UAAU,KAAK,SAAU,SAAQ,CAAG9B,EAAA,MAAS,KAAA,CAAI,EAAI,eAAY,EAAAqF,EAAA,CAAA,GAEjFrD,EAKEyC,EAAA,CAJA,KAAK,sBACL,OAAO,OACP,KAAK,cACJ,WAAczE,EAAA,OAAQ,WAAA,CAAA,6BAQ/BgC,EAsBamB,EAAAmC,EAAA,EAAA,CAtBA,OAAQrF,EAAA,MAAQ,gBAAW2C,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAGY,GAAU,CAAMA,IAAMvD,EAAA,MAAK,MAAA,aACpE,IAoBe,CApBf+B,EAoBemB,EAAAoC,EAAA,EAAA,KAAA,WAnBb,IAA0C,CAA1CvD,EAA0CmB,EAAAqC,EAAA,EAAA,CAA3B,MAAM,mBAAkB,EAClBvF,EAAA,WAArBiC,EAiBgBiB,EAAAsC,EAAA,EAAA,OAjBY,MAAM,QAAQ,aAAW,iBAAA,aACnD,IAAA,SAA2E,OAA3EzD,EAA2EmB,EAAAuC,EAAA,EAAA,CAA9D,GAAG,MAAI,WAAC,IAAoB,GAApB,uBAAoBpD,EAAGrC,EAAA,MAAM,QAAQ,EAAA,CAAA,CAAA,SAE1D6B,EAGI,IAHJ6D,GAAgB,UACRrD,EAAGrC,EAAA,MAAM,OAAOkD,EAAAA,EAAA3B,CAAA,EAAU,OAAV2B,YAAAA,EAAgB,IAAE,MAAA,MAAA,EAAoB,+BAC5Db,EAAGrC,EAAA,MAAM,OAAOkD,EAAAA,EAAA3B,CAAA,EAAU,OAAV2B,YAAAA,EAAgB,IAAE,OAAA,OAAA,EAAsB,YAC1D,CAAA,EACArB,EAGQ,QAHR8D,GAGQ,CAFNhD,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAAd,EAAyB,YAAnB,eAAY,EAAA,KAClBA,EAAuE,QAAA,sCAAvD5B,EAAO,MAAA2D,GAAE,KAAK,WAAW,aAAa,cAAA,gBAAtC3D,EAAA,KAAO,CAAA,KAEzB4B,EAKM,MALN+D,GAKM,CAJJ/D,EAES,SAAA,CAFD,MAAM,UAAW,SAAU5B,EAAA,MAAQ,OAAM,EAAO,QAAOwB,CAAA,EAAgB,oBAE/E,EAAAoE,EAAA,EACA9D,EAA+CmB,EAAA4C,EAAA,EAAA,CAAlC,MAAM,SAAO,WAAC,IAAM,CAAA,GAAAnD,EAAA,EAAA,IAAAA,EAAA,EAAA,EAAA,GAAN,SAAM,EAAA,CAAA"}
|
|
1
|
+
{"version":3,"file":"UsersView-BnN_hvh2.js","sources":["../../src/views/UsersView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Accounts and personal API tokens.\n *\n * Accounts landed in 0.10.0 with no UI at all, so the only way to add a second operator was a curl\n * call — which is exactly the thing a control plane exists to spare people.\n *\n * TWO AUDIENCES, TWO SECTIONS, and the split is deliberate. **Accounts** are about other people and\n * are shared state; **tokens** are yours alone (the API scopes them to the caller and cannot show\n * anyone else's). Mixing them into one table would imply an admin can manage another person's\n * tokens, which is not true and would be a bad idea if it were.\n *\n * SECRETS ARE SHOWN ONCE, and the page says so in the same breath as showing them. Same discipline\n * as a notifier signing secret and a registry credential: there is no read path on the server, so\n * this is not a policy that could be relaxed later — the value genuinely stops existing.\n *\n * ── ROLES ────────────────────────────────────────────────────────────────────────────────────────\n *\n * The roster is ordinary team information, so ANY signed-in account reads this page. Changing it —\n * adding an account, setting a role, deleting one, setting someone ELSE's password — is admin's,\n * and those controls are simply not rendered for anyone else. Your OWN password stays yours at\n * every role, so that one button is there for everybody, on their own row only.\n *\n * Hiding is UX. `can('admin')` decides what to DRAW; the server's permission table decides what is\n * allowed, and a hidden control that is somehow reached still comes back 403 — which lands in\n * `listError` and says which role the route wanted. That path is not a fallback, it is the actual\n * enforcement, so it stays.\n *\n * THE LAST ADMIN is the one rule the server has that the UI can see coming: it refuses to delete or\n * demote the final admin account. A request that can only fail is worse than a control that says\n * why it cannot be pressed, so that row's role picker and Delete arrive already disabled, carrying\n * the same remedy the server would have answered with — promote another account first.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport { ROLES, type Role, type User, type UsersResponse } from '../api/types';\nimport { authState, can, checkAuth } from '../composables/useAuth';\nimport { sentence } from '../composables/useFormat';\nimport {\n DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogClose,\n} from 'reka-ui';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport RelativeTime from '../components/RelativeTime.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\nimport RefreshButton from '../components/RefreshButton.vue';\nimport SelectMenu from '../components/SelectMenu.vue';\n\ntype Token = { id: number; name: string; createdAt: number; lastUsedAt: number | null };\n\nconst users = ref<User[]>([]);\nconst tokens = ref<Token[]>([]);\nconst loaded = ref(false);\nconst listError = ref('');\n\nconst newUser = ref<{ username: string; password: string; role: Role }>({\n username: '',\n password: '',\n role: 'viewer',\n});\nconst newToken = ref('');\nconst pwFor = ref<User | null>(null);\nconst pwValue = ref('');\n\n/** Held only until dismissed — there is no way to show either of these again. */\nconst revealed = ref<{ what: string; value: string; note: string } | null>(null);\n\nconst canCreateUser = computed(\n () => newUser.value.username.trim().length > 0 && newUser.value.password.length >= 8,\n);\n\n/** One line each, in the picker itself — a role name alone does not say what it costs to grant. */\nconst ROLE_WHAT: Record<Role, string> = {\n viewer: 'reads everything',\n developer: 'deploys and tears down',\n maintainer: 'host configuration',\n admin: 'accounts and sign-on',\n};\nconst roleOptions = ROLES.map((r) => ({ value: r, label: sentence(r), hint: ROLE_WHAT[r] }));\n\nconst adminCount = computed(() => users.value.filter((u) => u.role === 'admin').length);\n/**\n * The last admin cannot be demoted or deleted — the server refuses both, and a host with no admin\n * has nobody who can add one back. Counting ROWS is the same thing the server counts: root holds\n * PSTACK_TOKEN, not an account, and may not exist as a human at all.\n */\nfunction lastAdmin(u: User): boolean {\n return u.role === 'admin' && adminCount.value === 1;\n}\nconst LAST_ADMIN_WHY = 'The only admin — promote another account first.';\n\nasync function load(): Promise<void> {\n const [u, t] = await Promise.all([\n api.get<UsersResponse>('/api/users'),\n api.get<{ tokens: Token[] }>('/api/tokens'),\n ]);\n loaded.value = true;\n if (!u.ok) {\n listError.value = problem(u, 'load the accounts');\n return;\n }\n listError.value = '';\n users.value = u.body.users ?? [];\n // Root (PSTACK_TOKEN) is not a user, so it has no personal tokens — an error here is expected.\n tokens.value = t.ok ? (t.body.tokens ?? []) : [];\n}\nvoid load();\n\nasync function createUser(): Promise<void> {\n // The role is always sent, never left to the server's default: what this form creates should be\n // whatever the form is showing. (An absent role means viewer there, which is the same answer —\n // but the day it is not, this page would be creating something other than what it said.)\n const r = await api.post<{ user: User }>('/api/users', {\n username: newUser.value.username.trim(),\n password: newUser.value.password,\n role: newUser.value.role,\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this account');\n return;\n }\n toast('ok', `Added ${r.body.user.username}.`);\n newUser.value = { username: '', password: '', role: 'viewer' };\n void load();\n}\n\nasync function removeUser(u: User): Promise<void> {\n const r = await api.del(`/api/users/${u.id}`);\n // The server refuses to delete the last account. That is a rule worth surfacing verbatim rather\n // than as \"could not delete\" — it tells the operator what to do instead.\n if (!r.ok) {\n listError.value = problem(r, `delete ${u.username}`);\n return;\n }\n toast('ok', `Deleted ${u.username}.`);\n void load();\n}\n\nasync function changeRole(u: User, role: string): Promise<void> {\n if (role === u.role) return;\n const r = await api.patch(`/api/users/${u.id}`, { role });\n if (!r.ok) {\n listError.value = problem(r, `change the role for ${u.username}`);\n return;\n }\n listError.value = '';\n if (u.id === authState.user?.id) {\n // You just changed your OWN role, and this whole app — the rail, this page's controls, the\n // footer — is drawn from the role it is holding. Re-read it before anything renders again,\n // or the UI keeps offering what the server will now refuse. (The server needs no prompting:\n // every request looks the role up fresh.)\n await checkAuth();\n toast('ok', `You are now a ${role}.`);\n } else {\n toast('ok', `${u.username} is now a ${role}.`);\n }\n void load();\n}\n\nasync function changePassword(): Promise<void> {\n const u = pwFor.value;\n if (!u) return;\n const r = await api.put(`/api/users/${u.id}/password`, { password: pwValue.value });\n if (!r.ok) {\n listError.value = problem(r, `change the password for ${u.username}`);\n return;\n }\n pwFor.value = null;\n pwValue.value = '';\n // If it was YOU, the next request is a 401 — the toast says to sign in again so that reads as an\n // expected step rather than a bug. The dialog carried the full warning before the change.\n toast(\n 'ok',\n u.id === authState.user?.id ? `Changed — sign in again.` : `Password changed.`,\n );\n void load();\n}\n\nasync function createToken(): Promise<void> {\n const r = await api.post<{ token: string; id: number }>('/api/tokens', {\n name: newToken.value.trim(),\n });\n if (!r.ok) {\n listError.value = problem(r, 'create this token');\n return;\n }\n revealed.value = {\n what: `Personal token “${newToken.value.trim()}”`,\n value: r.body.token,\n note: 'Shown once — the server keeps only a hash. It can do anything your account can.',\n };\n newToken.value = '';\n void load();\n}\n\nasync function removeToken(t: Token): Promise<void> {\n const r = await api.del(`/api/tokens/${t.id}`);\n if (!r.ok) {\n listError.value = problem(r, `revoke ${t.name}`);\n return;\n }\n toast('ok', `Revoked ${t.name}.`);\n void load();\n}\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>Users & access</h1>\n <div class=\"sub\">Accounts and personal tokens</div>\n </div>\n <span class=\"grow\" />\n <RefreshButton :run=\"load\" />\n </div>\n\n <ErrorNote v-if=\"listError\" :text=\"listError\" title=\"Could not load the accounts.\" />\n\n <div v-if=\"revealed\" class=\"banner ok\">\n <b>{{ revealed.what }} — copy it now.</b>\n <!-- The one-line interlock: there is no read path on the server, so this really is the only\n time the value exists anywhere but the clipboard. -->\n <p>{{ revealed.note }}</p>\n <pre class=\"code\">{{ revealed.value }}</pre>\n <div class=\"row\">\n <button @click=\"revealed = null\">Dismiss</button>\n </div>\n </div>\n\n <div class=\"grid-2\">\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Accounts</h2>\n <span class=\"grow\" />\n <span class=\"mute\" style=\"font-size: var(--t-xs)\">{{ users.length }}</span>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"3\" tall />\n <!-- `cards` + `data-label` per cell: below 640px the same markup renders as cards. Without\n it the row's two buttons pushed the table past the viewport and got clipped. -->\n <table v-else-if=\"users.length\" role=\"table\" class=\"cards\">\n <thead role=\"rowgroup\">\n <tr role=\"row\">\n <th role=\"columnheader\">User</th>\n <th role=\"columnheader\">Role</th>\n <th role=\"columnheader\">Added</th>\n <th role=\"columnheader\" aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody role=\"rowgroup\">\n <tr v-for=\"u in users\" :key=\"u.id\" role=\"row\">\n <td role=\"cell\" class=\"name\" data-label=\"user\">\n <b>{{ u.username }}</b>\n <span v-if=\"u.id === authState.user?.id\" class=\"badge info\">you</span>\n </td>\n <!-- An admin gets the picker in place: promotion is a one-step decision about one\n person, and a dialog for a four-option choice is a page-blocking ceremony. Anyone\n else reads the role as text — `sentence()` at render only, never on the value.\n A role this build does not know still shows: it falls through to `placeholder`. -->\n <td role=\"cell\" data-label=\"role\">\n <span\n v-if=\"can('admin')\"\n class=\"role-pick\"\n :title=\"lastAdmin(u) ? LAST_ADMIN_WHY : undefined\"\n >\n <SelectMenu\n :model-value=\"u.role\"\n :label=\"`Role for ${u.username}`\"\n :options=\"roleOptions\"\n :disabled=\"lastAdmin(u)\"\n :placeholder=\"sentence(u.role)\"\n @update:model-value=\"(v) => changeRole(u, v)\"\n />\n </span>\n <span v-else>{{ sentence(u.role) }}</span>\n </td>\n <td role=\"cell\" class=\"dim nowrap\" data-label=\"added\"><RelativeTime :at=\"u.createdAt\" /></td>\n <td role=\"cell\" class=\"right nowrap\" data-label=\"\">\n <!-- Your own password is yours at any role; someone else's is an admin operation. -->\n <button\n v-if=\"u.id === authState.user?.id || can('admin')\"\n class=\"ghost sm\"\n @click=\"pwFor = u; pwValue = ''\"\n >\n Change password\n </button>\n <ActionButton\n v-if=\"can('admin')\"\n class=\"danger sm\"\n :confirm=\"`Delete ${u.username}?`\"\n :disabled=\"lastAdmin(u)\"\n :title=\"lastAdmin(u) ? LAST_ADMIN_WHY : undefined\"\n @run=\"removeUser(u)\"\n >Delete</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <p v-else class=\"hint\">No accounts yet.</p>\n\n <form v-if=\"can('admin')\" class=\"stack-form\" @submit.prevent=\"createUser\">\n <h3>Add an account</h3>\n <div class=\"row wrap two-up\">\n <label class=\"field grow\">\n <span>Username</span>\n <input v-model=\"newUser.username\" type=\"text\" autocomplete=\"off\" />\n </label>\n <label class=\"field grow\">\n <span>Password</span>\n <input v-model=\"newUser.password\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n </div>\n <div class=\"field\">\n <label for=\"new-role\">Role</label>\n <SelectMenu\n id=\"new-role\"\n :model-value=\"newUser.role\"\n label=\"Role for the new account\"\n :options=\"roleOptions\"\n @update:model-value=\"(v) => (newUser.role = v as Role)\"\n />\n </div>\n <p class=\"hint\">At least 8 characters.</p>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!canCreateUser\">Add account</button>\n <span class=\"grow\" />\n </div>\n <EquivalentCommand\n what=\"this account creation\"\n method=\"POST\"\n path=\"/api/users\"\n :body=\"{ username: newUser.username || 'alice', password: '…', role: newUser.role }\"\n />\n </form>\n <!-- Not an apology for a missing button: reading the roster is every account's, changing it\n is admin's, and the roster above already names who to ask. -->\n <p v-else class=\"hint\">Only an admin can add accounts or change roles.</p>\n </section>\n\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Your API tokens</h2>\n </div>\n\n <SkeletonList v-if=\"!loaded\" :rows=\"2\" tall />\n <table v-else-if=\"tokens.length\" role=\"table\" class=\"cards\">\n <thead role=\"rowgroup\">\n <tr role=\"row\">\n <th role=\"columnheader\">Name</th>\n <th role=\"columnheader\">Last used</th>\n <th role=\"columnheader\" aria-label=\"Actions\" />\n </tr>\n </thead>\n <tbody role=\"rowgroup\">\n <tr v-for=\"t in tokens\" :key=\"t.id\" role=\"row\">\n <td role=\"cell\" class=\"name\" data-label=\"name\"><b>{{ t.name }}</b></td>\n <td role=\"cell\" class=\"dim nowrap\" data-label=\"last used\">\n <RelativeTime v-if=\"t.lastUsedAt\" :at=\"t.lastUsedAt\" />\n <span v-else class=\"mute\">never</span>\n </td>\n <td role=\"cell\" class=\"right nowrap\" data-label=\"\">\n <ActionButton\n class=\"danger sm\"\n :confirm=\"`Revoke ${t.name}?`\"\n @run=\"removeToken(t)\"\n >Revoke</ActionButton\n >\n </td>\n </tr>\n </tbody>\n </table>\n <!-- Two different facts, not one empty list: the host token cannot own tokens at all. -->\n <p v-else class=\"hint\">\n {{ authState.root ? 'The host token has no personal tokens.' : 'No tokens yet — name one below.' }}\n </p>\n\n <form v-if=\"!authState.root\" class=\"stack-form\" @submit.prevent=\"createToken\">\n <h3>New token</h3>\n <label class=\"field\">\n <span>Name</span>\n <input v-model=\"newToken\" type=\"text\" placeholder=\"ci-deploy\" autocomplete=\"off\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" type=\"submit\" :disabled=\"!newToken.trim()\">Create token</button>\n </div>\n <EquivalentCommand\n what=\"this token creation\"\n method=\"POST\"\n path=\"/api/tokens\"\n :body=\"{ name: newToken || 'ci-deploy' }\"\n />\n </form>\n </section>\n </div>\n\n <!-- Password change: a dialog, because it is a distinct decision about one row. Reka owns\n modality — hand-rolling it declared `aria-modal` without the focus trap it promises. -->\n <DialogRoot :open=\"!!pwFor\" @update:open=\"(v: boolean) => !v && (pwFor = null)\">\n <DialogPortal>\n <DialogOverlay class=\"scrim help-scrim\" />\n <DialogContent v-if=\"pwFor\" class=\"sheet\" aria-label=\"Change password\">\n <DialogTitle as=\"h3\">Change password for {{ pwFor.username }}</DialogTitle>\n <!-- The interlock, one line: this ends every session and revokes every personal token. -->\n <p class=\"hint\">\n Signs {{ pwFor.id === authState.user?.id ? 'you' : 'them' }} out everywhere and revokes\n {{ pwFor.id === authState.user?.id ? 'your' : 'their' }} tokens.\n </p>\n <label class=\"field\">\n <span>New password</span>\n <input v-model=\"pwValue\" type=\"password\" autocomplete=\"new-password\" />\n </label>\n <div class=\"row\">\n <button class=\"primary\" :disabled=\"pwValue.length < 8\" @click=\"changePassword\">\n Change password\n </button>\n <DialogClose class=\"ghost\">Cancel</DialogClose>\n </div>\n </DialogContent>\n </DialogPortal>\n </DialogRoot>\n </div>\n</template>\n\n<style scoped>\n.stack-form {\n margin-top: var(--s5);\n padding-top: var(--s4);\n border-top: 1px solid var(--line-soft);\n display: flex;\n flex-direction: column;\n gap: var(--s3);\n}\n.stack-form h3 {\n font-size: var(--t-sm);\n color: var(--fg-dim);\n}\ntd.right {\n text-align: end;\n}\n/* Two fields share a line where there is room and stack where there is not — at 390px a pair of\n * half-width inputs is two inputs nobody can type into. */\n.two-up > .field {\n flex: 1 1 200px;\n}\ntd.right button {\n margin-inline-start: var(--s2);\n}\n/* The picker sits in a row, not a form: it takes the width its longest label needs and no more,\n * so the Role column does not eat the table. The wrapper exists to carry `title` when the control\n * inside it is disabled — a disabled button does not reliably show one of its own. */\n.role-pick {\n display: inline-block;\n}\n.role-pick :deep(.selm-trigger) {\n width: auto;\n min-width: 150px;\n}\n</style>\n"],"names":["LAST_ADMIN_WHY","users","ref","tokens","loaded","listError","newUser","newToken","pwFor","pwValue","revealed","canCreateUser","computed","ROLE_WHAT","roleOptions","ROLES","r","sentence","adminCount","u","lastAdmin","load","t","api","problem","createUser","toast","removeUser","changeRole","role","_a","authState","checkAuth","changePassword","createToken","removeToken","_createElementBlock","_createElementVNode","_hoisted_1","_createVNode","RefreshButton","_createBlock","ErrorNote","_openBlock","_hoisted_2","_toDisplayString","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","_cache","_hoisted_8","_hoisted_9","_hoisted_10","_Fragment","_renderList","_hoisted_11","_unref","_hoisted_12","_hoisted_13","can","SelectMenu","v","_hoisted_15","_hoisted_16","RelativeTime","_hoisted_17","$event","_hoisted_18","ActionButton","_hoisted_19","SkeletonList","_hoisted_20","_hoisted_21","_vModelText","_hoisted_22","_hoisted_23","_hoisted_24","_hoisted_25","EquivalentCommand","_hoisted_26","_hoisted_27","_hoisted_28","_hoisted_29","_hoisted_30","_hoisted_31","_hoisted_32","_hoisted_33","_hoisted_34","_hoisted_35","_hoisted_36","_hoisted_37","DialogRoot","DialogPortal","DialogOverlay","DialogContent","DialogTitle","_hoisted_38","_hoisted_39","_hoisted_40","_hoisted_41","DialogClose"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{V as f,m as k}from"./VarIO-
|
|
2
|
-
//# sourceMappingURL=VarEditor.vue_vue_type_script_setup_true_lang-
|
|
1
|
+
import{V as f,m as k}from"./VarIO-CNugbzeb.js";import{d as b,c as p,a as l,F as g,r as x,e as y,h as _,o as c,n as m}from"./index-DlhYSSKq.js";const w=["value","aria-label","onInput"],I=["value","aria-label","onInput"],C=["aria-label","onClick"],N=b({__name:"VarEditor",props:{modelValue:{}},emits:["update:modelValue","change"],setup(r,{emit:i}){const u=r,t=i;function d(o,a){const n=u.modelValue.map((e,s)=>s===o?{...e,...a}:e);t("update:modelValue",n),t("change")}function v(){t("update:modelValue",[...u.modelValue,{k:"",v:""}]),t("change")}function h(o,a){const n=a==="replace"?o:k(u.modelValue,o);t("update:modelValue",n.length?n:[{k:"",v:""}]),t("change")}function V(o){const a=u.modelValue.filter((n,e)=>e!==o);t("update:modelValue",a.length?a:[{k:"",v:""}]),t("change")}return(o,a)=>(c(),p("div",null,[a[1]||(a[1]=l("div",{class:"kv head"},[l("span",null,"name"),l("span",null,"value"),l("span")],-1)),(c(!0),p(g,null,x(r.modelValue,(n,e)=>(c(),p("div",{key:e,class:"kv"},[l("input",{type:"text",value:n.k,"aria-label":`variable name ${e+1}`,placeholder:"PR",spellcheck:"false",autocomplete:"off",onInput:s=>d(e,{k:s.target.value.trim()})},null,40,w),l("input",{type:"text",value:n.v,"aria-label":`variable value ${e+1}`,placeholder:"7",spellcheck:"false",autocomplete:"off",onInput:s=>d(e,{v:s.target.value})},null,40,I),l("button",{class:"sm ghost","aria-label":`remove variable ${e+1}`,onClick:s=>V(e)},"−",8,C)]))),128)),l("button",{class:"sm",onClick:v},"+ variable"),y(f,{pairs:r.modelValue,replace:"",onApply:h},{default:_(()=>[...a[0]||(a[0]=[l("p",{class:"hint",style:{"margin-top":"0"}},[l("b",null,"Replace all"),m(" uses exactly what was pasted. "),l("b",null,"Merge"),m(" overwrites the names it finds, keeps the rest, and adds what is new. ")],-1)])]),_:1},8,["pairs"])]))}});export{N as _};
|
|
2
|
+
//# sourceMappingURL=VarEditor.vue_vue_type_script_setup_true_lang-Dg_ORvZN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VarEditor.vue_vue_type_script_setup_true_lang-
|
|
1
|
+
{"version":3,"file":"VarEditor.vue_vue_type_script_setup_true_lang-Dg_ORvZN.js","sources":["../../src/components/VarEditor.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * The free-form variable editor, shared by the Config tab and Submit.\n *\n * Free-form on purpose: the spec's `env:` block lists what it DECLARES, which is not the same as\n * what it NEEDS — `stack: pr-${PR}` consumes `PR` without declaring it anywhere. The authoritative\n * list of what is missing is the server's 400 text, not this form.\n *\n * The rows are the editor; `VarIO` under them is the same list as `.env`, CSV or TSV. Typing a\n * dozen variables one input at a time is how they end up mistyped, and the list an operator\n * already has is almost always one of those three formats.\n */\nimport type { VarPair } from '../api/types';\nimport { mergeVars } from '../composables/useVarFormats';\nimport VarIO from './VarIO.vue';\n\nconst props = defineProps<{ modelValue: VarPair[] }>();\nconst emit = defineEmits<{ 'update:modelValue': [VarPair[]]; change: [] }>();\n\nfunction set(i: number, patch: Partial<VarPair>): void {\n const next = props.modelValue.map((e, n) => (n === i ? { ...e, ...patch } : e));\n emit('update:modelValue', next);\n emit('change');\n}\n\nfunction add(): void {\n emit('update:modelValue', [...props.modelValue, { k: '', v: '' }]);\n emit('change');\n}\n\n/**\n * Both modes are honest here — this editor owns its whole list, so replacing it is a thing the\n * operator can mean. A replace that parsed to nothing still leaves one blank row: an editor with no\n * rows looks broken.\n */\nfunction applyImport(pairs: VarPair[], mode: 'replace' | 'merge'): void {\n const next = mode === 'replace' ? pairs : mergeVars(props.modelValue, pairs);\n emit('update:modelValue', next.length ? next : [{ k: '', v: '' }]);\n emit('change');\n}\n\nfunction remove(i: number): void {\n const next = props.modelValue.filter((_, n) => n !== i);\n // Never leave the editor with no rows: an empty list looks like a broken control.\n emit('update:modelValue', next.length ? next : [{ k: '', v: '' }]);\n emit('change');\n}\n</script>\n\n<template>\n <div>\n <div class=\"kv head\"><span>name</span><span>value</span><span /></div>\n <div v-for=\"(e, i) in modelValue\" :key=\"i\" class=\"kv\">\n <input\n type=\"text\"\n :value=\"e.k\"\n :aria-label=\"`variable name ${i + 1}`\"\n placeholder=\"PR\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n @input=\"set(i, { k: ($event.target as HTMLInputElement).value.trim() })\"\n />\n <input\n type=\"text\"\n :value=\"e.v\"\n :aria-label=\"`variable value ${i + 1}`\"\n placeholder=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n @input=\"set(i, { v: ($event.target as HTMLInputElement).value })\"\n />\n <button class=\"sm ghost\" :aria-label=\"`remove variable ${i + 1}`\" @click=\"remove(i)\">−</button>\n </div>\n <button class=\"sm\" @click=\"add\">+ variable</button>\n\n <VarIO :pairs=\"modelValue\" replace @apply=\"applyImport\">\n <p class=\"hint\" style=\"margin-top: 0\">\n <b>Replace all</b> uses exactly what was pasted. <b>Merge</b> overwrites the names it finds,\n keeps the rest, and adds what is new.\n </p>\n </VarIO>\n </div>\n</template>\n"],"names":["props","__props","emit","__emit","set","i","patch","next","n","add","applyImport","pairs","mode","mergeVars","remove","_","_createElementBlock","_createElementVNode","_openBlock","_Fragment","_renderList","e","$event","_hoisted_3","_createVNode","VarIO","_cache"],"mappings":"2VAgBA,MAAMA,EAAQC,EACRC,EAAOC,EAEb,SAASC,EAAIC,EAAWC,EAA+B,CACrD,MAAMC,EAAOP,EAAM,WAAW,IAAI,CAAC,EAAGQ,IAAOA,IAAMH,EAAI,CAAE,GAAG,EAAG,GAAGC,CAAA,EAAU,CAAE,EAC9EJ,EAAK,oBAAqBK,CAAI,EAC9BL,EAAK,QAAQ,CACf,CAEA,SAASO,GAAY,CACnBP,EAAK,oBAAqB,CAAC,GAAGF,EAAM,WAAY,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEE,EAAK,QAAQ,CACf,CAOA,SAASQ,EAAYC,EAAkBC,EAAiC,CACtE,MAAML,EAAOK,IAAS,UAAYD,EAAQE,EAAUb,EAAM,WAAYW,CAAK,EAC3ET,EAAK,oBAAqBK,EAAK,OAASA,EAAO,CAAC,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEL,EAAK,QAAQ,CACf,CAEA,SAASY,EAAOT,EAAiB,CAC/B,MAAME,EAAOP,EAAM,WAAW,OAAO,CAACe,EAAGP,IAAMA,IAAMH,CAAC,EAEtDH,EAAK,oBAAqBK,EAAK,OAASA,EAAO,CAAC,CAAE,EAAG,GAAI,EAAG,EAAA,CAAI,CAAC,EACjEL,EAAK,QAAQ,CACf,mBAIEc,EA+BM,MAAA,KAAA,aA9BJC,EAAsE,MAAA,CAAjE,MAAM,WAAS,CAACA,EAAiB,YAAX,MAAI,EAAOA,EAAkB,YAAZ,OAAK,EAAOA,EAAQ,MAAA,CAAA,QAChEC,EAAA,EAAA,EAAAF,EAoBMG,EAAA,KAAAC,EApBgBnB,EAAA,WAAU,CAAnBoB,EAAGhB,SAAhBW,EAoBM,MAAA,CApB6B,IAAKX,EAAG,MAAM,IAAA,GAC/CY,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,8BAA6BhB,EAAC,CAAA,GAC/B,YAAY,KACZ,WAAW,QACX,aAAa,MACZ,QAAKiB,GAAElB,EAAIC,EAAC,CAAA,EAAQiB,EAAO,OAA4B,MAAM,MAAI,CAAA,CAAA,aAEpEL,EAQE,QAAA,CAPA,KAAK,OACJ,MAAOI,EAAE,EACT,+BAA8BhB,EAAC,CAAA,GAChC,YAAY,IACZ,WAAW,QACX,aAAa,MACZ,QAAKiB,GAAElB,EAAIC,KAASiB,EAAO,OAA4B,KAAA,CAAK,CAAA,aAE/DL,EAA+F,SAAA,CAAvF,MAAM,WAAY,gCAA+BZ,EAAC,CAAA,GAAS,QAAKiB,GAAER,EAAOT,CAAC,CAAA,EAAG,IAAC,EAAAkB,CAAA,CAAA,WAExFN,EAAmD,SAAA,CAA3C,MAAM,KAAM,QAAOR,CAAA,EAAK,YAAU,EAE1Ce,EAKQC,EAAA,CALA,MAAOxB,EAAA,WAAY,QAAA,GAAS,QAAOS,CAAA,aACzC,IAGI,CAAA,GAAAgB,EAAA,CAAA,IAAAA,EAAA,CAAA,EAAA,CAHJT,EAGI,IAAA,CAHD,MAAM,OAAO,MAAA,CAAA,aAAA,GAAA,CAAA,GACdA,EAAkB,SAAf,aAAW,IAAI,iCAA+B,EAAAA,EAAY,SAAT,OAAK,IAAI,wEAE/D,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{d as P,c as h,a as c,F as $,r as S,u as C,t as b,f as w,Y as Z,g as Q,v as W,e as q,n as M,E as H,i as V,j as A,o as m,a6 as N}from"./index-
|
|
1
|
+
import{d as P,c as h,a as c,F as $,r as S,u as C,t as b,f as w,Y as Z,g as Q,v as W,e as q,n as M,E as H,i as V,j as A,o as m,a6 as N}from"./index-DlhYSSKq.js";import{_ as U}from"./SelectMenu.vue_vue_type_script_setup_true_lang-PIaLSNsk.js";import{_ as G}from"./_plugin-vue_export-helper-DlAUqK2U.js";const T=["env","csv","tsv"],x={env:".env",csv:"CSV",tsv:"TSV"},D=/^[A-Za-z_][A-Za-z0-9_]*$/,Y=/^[A-Za-z0-9_@%+=:,./-]+$/,F={csv:",",tsv:" "},J=new Set(["name","key","variable","var"]),K=new Set(["value","val"]);function X(e,s,t){const a=e.filter(l=>l.k);if(s==="env")return[...t?t.split(`
|
|
2
2
|
`).map(o=>`# ${o}`):[],...a.map(o=>`${o.k}=${ee(o.v)}`)].join(`
|
|
3
3
|
`)+`
|
|
4
4
|
`;const n=F[s];return[["name","value"],...a.map(l=>[l.k,l.v])].map(l=>l.map(o=>te(o??"",n)).join(n)).join(`
|
|
@@ -12,4 +12,4 @@ import{d as P,c as h,a as c,F as $,r as S,u as C,t as b,f as w,Y as Z,g as Q,v a
|
|
|
12
12
|
REGION=eu-central
|
|
13
13
|
|
|
14
14
|
…or two columns of CSV or TSV`},null,512),[[W,l.value]]),c("div",he,[q(U,{modelValue:o.value,"onUpdate:modelValue":r[3]||(r[3]=u=>o.value=u),label:"how to read the paste",options:k},null,8,["modelValue"]),i.value?(m(),h("span",me," read as "+b(C(x)[i.value.format]),1)):w("",!0)]),i.value?(m(),h($,{key:0},[c("p",be,[c("b",null,b(d.value.length),1),M(" variable"+b(d.value.length===1?"":"s")+" read. ",1),y.value.length?(m(),h($,{key:0},[M(b(y.value.length)+" ("+b(y.value.map(u=>u.k).join(", "))+") had no value and will be skipped — a value stored here is never blank. ",1)],64)):w("",!0)]),d.value.length?(m(),h("ul",ge,[(m(!0),h($,null,S(d.value.slice(0,R),u=>(m(),h("li",{key:u.k},[c("span",ye,[c("code",null,b(u.k),1)]),c("span",ke,b(u.v),1)]))),128)),d.value.length>R?(m(),h("li",we," and "+b(d.value.length-R)+" more ",1)):w("",!0)])):w("",!0),i.value.problems.length?(m(),h("div",_e,[c("b",null,b(i.value.problems.length)+" line"+b(i.value.problems.length===1?"":"s")+" could not be read ",1),c("ul",$e,[(m(!0),h($,null,S(i.value.problems,u=>(m(),h("li",{key:u.line},[c("span",xe,"line "+b(u.line),1),c("code",null,b(u.text),1),c("span",Ee,b(u.reason),1)]))),128))]),r[10]||(r[10]=c("p",null," They are not imported. Fix them and paste again, or apply what parsed and add the rest by hand. ",-1))])):w("",!0),c("div",Ce,[e.replace?(m(),h("button",{key:0,type:"button",class:"primary",disabled:!d.value.length||e.busy,title:d.value.length?"Discard the current list and use exactly what was pasted":"Nothing parsed to apply",onClick:r[4]||(r[4]=u=>_("replace"))}," Replace all ",8,Ve)):w("",!0),c("button",{type:"button",class:H(e.replace?"":"primary"),disabled:!d.value.length||e.busy,title:d.value.length?"Overwrite matching names, keep the rest, add what is new":"Nothing parsed to apply",onClick:r[5]||(r[5]=u=>_("merge"))}," Merge ",10,Ae),c("button",{type:"button",class:"ghost",onClick:r[6]||(r[6]=u=>n.value=!1)},"Cancel")])],64)):w("",!0)])):w("",!0)]))}}),Oe=G(Se,[["__scopeId","data-v-0ae3954c"]]);export{Oe as V,Le as m};
|
|
15
|
-
//# sourceMappingURL=VarIO-
|
|
15
|
+
//# sourceMappingURL=VarIO-CNugbzeb.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VarIO-BFBaI8oU.js","sources":["../../src/composables/useVarFormats.ts","../../src/components/VarIO.vue"],"sourcesContent":["/**\n * `.env`, CSV and TSV for variable lists — the one implementation both variable surfaces share.\n *\n * WHY PURE FUNCTIONS IN A `.ts`. There is no component test harness in this app, so anything that\n * can be wrong in an interesting way has to live somewhere `bun test` can reach it. Everything here\n * is a string in and a value out; the components own the chrome and the policy, and nothing else.\n *\n * THE ROUND-TRIP IS THE CONTRACT. `parseVars(formatVars(pairs, f), f)` returns `pairs` for every\n * value the editors can hold — newlines, quotes, `#`, leading and trailing spaces, backslashes,\n * `=`, the empty string. That is what makes \"copy them out, paste them back\" safe, and it is what\n * `test/useVarFormats.test.ts` spends most of its lines on. Two deliberate exceptions, both tested:\n *\n * - A lone carriage return survives `.env` (escaped as `\\r`) but not CSV/TSV, where CRLF and CR\n * are normalised to LF before parsing — a row terminator cannot also be data without a\n * dialect flag, and no `<input type=\"text\">` can produce one anyway.\n * - `parseVars` collapses a repeated name to its last value, as every dotenv reader does. The\n * import preview shows the result, so the collapse is visible before anything is replaced.\n *\n * WHY CSV AND TSV ALWAYS EXPORT A `name,value` HEADER. A header row is ambiguous with data — a\n * variable really can be called `name` — and guessing wrong either eats a variable or imports a\n * label. Emitting the header makes our own output unambiguous: the header we wrote is the row the\n * detector eats, so `{ k: 'name', v: 'value' }` round-trips. Foreign input whose first row happens\n * to read `name,value` loses that row, which is why the importer previews what it parsed.\n *\n * A NAME MUST BE ONE A SPEC CAN REFERENCE. `VarEditor` only trims what is typed, so `foo-bar` can\n * exist in the editor and be exported — but `${foo-bar}` is not a thing `internal/spec` can\n * interpolate, so importing it would add a row that can never do anything. It comes back as a\n * problem instead, named, where the operator can see it.\n *\n * A MALFORMED LINE IS NEVER DROPPED SILENTLY. Every line that does not become a pair comes back in\n * `problems` with its line number, its text and why — a paste that half-worked without saying so\n * is how a deployment ends up missing the one variable its stack name is built from.\n */\n\nimport type { VarPair } from '../api/types';\n\nexport type VarFormat = 'env' | 'csv' | 'tsv';\n\n/** A line that did not become a pair. Reported, never swallowed. */\nexport type VarProblem = { line: number; text: string; reason: string };\n\nexport type VarParse = { format: VarFormat; pairs: VarPair[]; problems: VarProblem[] };\n\nexport const VAR_FORMATS: VarFormat[] = ['env', 'csv', 'tsv'];\nexport const FORMAT_LABEL: Record<VarFormat, string> = { env: '.env', csv: 'CSV', tsv: 'TSV' };\n\n/** The charset a spec can reference — `internal/spec`'s `${NAME}` regex, not a UI opinion. */\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n/** Values needing no quotes: bytes that dotenv, compose and a shell all read back identically. */\nconst PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;\nconst DELIM: Record<'csv' | 'tsv', string> = { csv: ',', tsv: '\\t' };\nconst HEADER_NAME = new Set(['name', 'key', 'variable', 'var']);\nconst HEADER_VALUE = new Set(['value', 'val']);\n\n// ── export ────────────────────────────────────────────────────────────────────────────────────────\n\n/**\n * Render pairs. `note` becomes `#` comment lines and is only expressible in `.env` — the surface\n * that passes one (host secrets, whose values are withheld) also says so on the page, because CSV\n * has nowhere to put it.\n */\nexport function formatVars(pairs: VarPair[], format: VarFormat, note?: string): string {\n const rows = pairs.filter((e) => e.k);\n if (format === 'env') {\n const head = note ? note.split('\\n').map((l) => `# ${l}`) : [];\n return [...head, ...rows.map((e) => `${e.k}=${envQuote(e.v)}`)].join('\\n') + '\\n';\n }\n const d = DELIM[format];\n return [['name', 'value'], ...rows.map((e) => [e.k, e.v])]\n .map((f) => f.map((v) => delimQuote(v ?? '', d)).join(d))\n .join('\\n') + '\\n';\n}\n\nfunction envQuote(v: string): string {\n // An empty value stays bare: `KEY=` parses back to `''` and reads as \"fill this in\", which is\n // exactly what a withheld secret is.\n if (v === '' || PLAIN.test(v)) return v;\n return `\"${v.replace(/[\\\\\"]/g, '\\\\$&').replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r')}\"`;\n}\n\nfunction delimQuote(v: string, d: string): string {\n const safe =\n v === '' || (!v.includes(d) && !v.includes('\"') && !/[\\n\\r]/.test(v) && v === v.trim());\n return safe ? v : `\"${v.replace(/\"/g, '\"\"')}\"`;\n}\n\n// ── import ────────────────────────────────────────────────────────────────────────────────────────\n\n/** Parse in `format`, or in whatever `detectFormat` reads the text as. */\nexport function parseVars(text: string, format?: VarFormat): VarParse {\n const f = format ?? detectFormat(text);\n return f === 'env' ? parseEnv(text) : parseDelimited(text, f);\n}\n\n/**\n * Which format is this? The first meaningful line decides `.env`; otherwise both delimiters are\n * tried with the real splitter and scored, because `indexOf('\\t')` calls a CSV whose value contains\n * a tab a TSV. A field that still holds a `\"` after splitting is the tell that the row was cut on\n * the wrong delimiter, so it counts against that reading.\n */\nexport function detectFormat(text: string): VarFormat {\n const first = text.split(/\\r\\n|\\n|\\r/).find((l) => l.trim() && !l.trim().startsWith('#')) ?? '';\n if (/^(export\\s+)?[A-Za-z_][A-Za-z0-9_]*\\s*=/.test(first.trim())) return 'env';\n const tsv = score(text, '\\t');\n const csv = score(text, ',');\n if (tsv <= 0 && csv <= 0) return 'env';\n return tsv > csv ? 'tsv' : 'csv';\n}\n\nfunction score(text: string, d: string): number {\n let s = 0;\n for (const r of splitRows(text, d)) {\n const f = padOrTrim(r.fields);\n if (!f.length) continue;\n s += f.length === 2 ? 1 : -1;\n for (const v of f) if (v.includes('\"')) s -= 1;\n }\n return s;\n}\n\nfunction parseEnv(text: string): VarParse {\n const lines = text.split(/\\r\\n|\\n|\\r/);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i] ?? '';\n const at = i + 1;\n if (!raw.trim() || raw.trim().startsWith('#')) continue;\n\n // Only the LEFT is trimmed: a quoted value that spans lines keeps the spaces before its first\n // newline, which `raw.trim()` would have eaten.\n const s = raw.trimStart().replace(/^export\\s+/, '');\n const eq = s.indexOf('='); // the FIRST `=`; the rest of the line is value, `=` signs and all\n if (eq < 0) {\n problems.push({ line: at, text: raw, reason: 'no \"=\" — a line reads NAME=value' });\n continue;\n }\n const k = s.slice(0, eq).trim();\n if (!NAME.test(k)) {\n problems.push({ line: at, text: raw, reason: nameReason(k) });\n continue;\n }\n\n const rest = s.slice(eq + 1).replace(/^[ \\t]+/, '');\n const q = rest[0];\n if (q === '\"' || q === \"'\") {\n const start = i;\n let body = rest.slice(1);\n let end = closingQuote(body, q);\n while (end < 0 && i + 1 < lines.length) {\n i++;\n body += '\\n' + (lines[i] ?? '');\n end = closingQuote(body, q);\n }\n if (end < 0) {\n // Rewind. The lines swallowed hunting for a closing quote are other people's lines, and\n // eating the rest of the file behind one stray `\"` is the silent drop this reports instead.\n i = start;\n problems.push({ line: at, text: raw, reason: 'the quote is never closed' });\n continue;\n }\n const inner = body.slice(0, end);\n // Single quotes are literal, double quotes carry escapes — dotenv's rule, and the one our\n // own exporter writes against. Anything after the closing quote is a trailing comment.\n pairs.push({ k, v: q === '\"' ? unescape(inner) : inner });\n continue;\n }\n // Unquoted: a ` #` starts a comment, trailing whitespace is not value.\n pairs.push({ k, v: rest.replace(/\\s+#.*$/, '').trimEnd() });\n }\n return { format: 'env', pairs: lastWins(pairs), problems };\n}\n\nconst ESCAPES: Record<string, string> = {\n n: '\\n',\n r: '\\r',\n t: '\\t',\n '\\\\': '\\\\',\n '\"': '\"',\n \"'\": \"'\",\n $: '$',\n '`': '`',\n};\n\nfunction unescape(s: string): string {\n let out = '';\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (c !== '\\\\' || i === s.length - 1) {\n out += c;\n continue;\n }\n const n = s[++i] ?? '';\n // An unknown escape keeps both characters: `C:\\path` typed inside quotes stays `C:\\path`\n // rather than losing the backslash to a rule nobody wrote down.\n out += ESCAPES[n] ?? '\\\\' + n;\n }\n return out;\n}\n\nfunction closingQuote(s: string, q: string): number {\n for (let i = 0; i < s.length; i++) {\n const c = s[i];\n if (q === '\"' && c === '\\\\') {\n i++;\n continue;\n }\n if (c === q) return i;\n }\n return -1;\n}\n\nfunction parseDelimited(text: string, format: 'csv' | 'tsv'): VarParse {\n const rows = splitRows(text, DELIM[format]);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n let first = true;\n\n for (const r of rows) {\n const fields = padOrTrim(r.fields);\n if (!fields.length) continue; // a blank line is not malformed\n if (first) {\n first = false;\n const a = (fields[0] ?? '').toLowerCase();\n const b = (fields[1] ?? '').toLowerCase();\n if (fields.length === 2 && HEADER_NAME.has(a) && HEADER_VALUE.has(b)) continue;\n }\n if (fields.length !== 2) {\n const n = fields.length;\n problems.push({\n line: r.line,\n text: r.raw,\n reason: `${n} column${n === 1 ? '' : 's'} — expected two, a name and a value`,\n });\n continue;\n }\n const k = (fields[0] ?? '').trim();\n if (!NAME.test(k)) {\n problems.push({ line: r.line, text: r.raw, reason: nameReason(k) });\n continue;\n }\n pairs.push({ k, v: fields[1] ?? '' });\n }\n return { format, pairs: lastWins(pairs), problems };\n}\n\ntype Row = { fields: string[]; line: number; raw: string };\n\n/**\n * RFC 4180 with one delimiter parameter: `\"\"` is a literal quote, a quoted field may hold the\n * delimiter and newlines, an unquoted field is trimmed. CRLF and lone CR are normalised to LF\n * first — a row terminator cannot also be data.\n */\nfunction splitRows(text: string, d: string): Row[] {\n const s = text.replace(/\\r\\n?/g, '\\n');\n const rows: Row[] = [];\n let fields: string[] = [];\n let field = '';\n let quoted = false;\n let inQuote = false;\n let line = 1;\n let rowLine = 1;\n let rowStart = 0;\n\n const endField = (): void => {\n fields.push(quoted ? field : field.trim());\n field = '';\n quoted = false;\n };\n\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (inQuote) {\n if (c === '\"') {\n if (s[i + 1] === '\"') {\n field += '\"';\n i++;\n } else inQuote = false;\n } else {\n if (c === '\\n') line++;\n field += c;\n }\n continue;\n }\n // A quote opens a field only at its start — spaces before it are formatting, a quote after\n // real characters is data.\n if (c === '\"' && !quoted && field.trim() === '') {\n field = '';\n inQuote = true;\n quoted = true;\n continue;\n }\n if (c === d) {\n endField();\n continue;\n }\n if (c === '\\n') {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart, i) });\n fields = [];\n line++;\n rowLine = line;\n rowStart = i + 1;\n continue;\n }\n field += c;\n }\n if (field !== '' || quoted || fields.length) {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart) });\n }\n return rows;\n}\n\n/**\n * Drop trailing empty columns a spreadsheet padded the row with — but never below two, or\n * `SECRET,` (a name whose value was withheld) would read as a one-column row instead of an empty\n * value. An all-empty row is a blank line.\n */\nfunction padOrTrim(fields: string[]): string[] {\n let n = fields.length;\n while (n > 0 && (fields[n - 1] ?? '') === '') n--;\n return n === 0 ? [] : fields.slice(0, Math.max(n, Math.min(2, fields.length)));\n}\n\nfunction nameReason(k: string): string {\n return k\n ? `\"${k}\" is not a usable variable name — letters, digits and _, not starting with a digit`\n : 'no name before the value';\n}\n\n/** dotenv semantics: a repeated name keeps its last value, at the position it first appeared. */\nfunction lastWins(pairs: VarPair[]): VarPair[] {\n const m = new Map<string, string>();\n for (const e of pairs) m.set(e.k, e.v);\n return [...m].map(([k, v]) => ({ k, v }));\n}\n\n/** Incoming overrides by name, everything else keeps its place. Blank editor rows are dropped. */\nexport function mergeVars(existing: VarPair[], incoming: VarPair[]): VarPair[] {\n const next = existing.filter((e) => e.k).map((e) => ({ ...e }));\n for (const e of incoming) {\n const hit = next.find((n) => n.k === e.k);\n if (hit) hit.v = e.v;\n else next.push({ ...e });\n }\n return next;\n}\n","<script setup lang=\"ts\">\n/**\n * Copy a variable list out as `.env`, CSV or TSV; paste one back in. The chrome only — every rule\n * about what those formats mean lives in `composables/useVarFormats.ts`, which is tested directly\n * because this component cannot be.\n *\n * NOTHING IS REPLACED BEFORE IT IS SHOWN. The paste box parses as you type and renders what it\n * read, including the lines it could not read, and only then offers the buttons that change\n * anything. A paste that half-worked in silence is how a deployment loses the one variable its\n * stack name is built from — which is the leak this project exists to prevent, arriving through a\n * text box.\n *\n * MERGE AND REPLACE ARE BOTH REAL ANSWERS, so the operator picks. `replace` is offered only where\n * this component's owner can actually replace the whole list: on the host-variable page a\n * \"replace\" would have to DELETE the rows the paste omits, and a delete that happens because\n * something was missing from a paste is not a thing to offer behind a two-word button.\n *\n * THE CLIPBOARD IS NOT ALWAYS THERE. `navigator.clipboard` is undefined on a plain-http origin —\n * exactly how this app is reached through a loopback tunnel — and a secure origin can still refuse\n * the write. A failed copy therefore reveals a selected textarea instead of only apologising in a\n * toast: the text exists either way, only the shortcut is missing.\n */\nimport { computed, ref } from 'vue';\nimport type { VarPair } from '../api/types';\nimport {\n FORMAT_LABEL,\n VAR_FORMATS,\n formatVars,\n parseVars,\n type VarFormat,\n} from '../composables/useVarFormats';\nimport { toast } from '../composables/useToasts';\nimport SelectMenu from './SelectMenu.vue';\n\nconst props = defineProps<{\n /** What \"copy\" exports. Blank rows are dropped by the formatter. */\n pairs: VarPair[];\n /** `#` comment lines on the `.env` export — the withheld-secrets surface passes one. */\n note?: string;\n /** Offer \"Replace all\". See the header: not every owner can honestly replace. */\n replace?: boolean;\n /** Skip parsed entries with no value, and say so. For a store that refuses an empty value. */\n skipEmpty?: boolean;\n /** An import is in flight upstream. */\n busy?: boolean;\n}>();\n\nconst emit = defineEmits<{ apply: [pairs: VarPair[], mode: 'replace' | 'merge'] }>();\n\nconst open = ref(false);\nconst text = ref('');\nconst fmt = ref('auto');\n/** The export text, revealed only when the clipboard refused it. */\nconst fallback = ref('');\n\nconst PREVIEW = 12;\n\nconst exportable = computed(() => props.pairs.filter((e) => e.k).length);\n\nconst parsed = computed(() =>\n text.value.trim()\n ? parseVars(text.value, fmt.value === 'auto' ? undefined : (fmt.value as VarFormat))\n : null,\n);\nconst skipped = computed(() =>\n props.skipEmpty && parsed.value ? parsed.value.pairs.filter((e) => !e.v) : [],\n);\nconst usable = computed(() =>\n parsed.value ? (props.skipEmpty ? parsed.value.pairs.filter((e) => e.v) : parsed.value.pairs) : [],\n);\n\nconst formatOptions = [\n { value: 'auto', label: 'Detect the format', hint: 'read the paste and decide' },\n ...VAR_FORMATS.map((f) => ({ value: f, label: `Read as ${FORMAT_LABEL[f]}` })),\n];\n\nasync function copy(f: VarFormat): Promise<void> {\n const out = formatVars(props.pairs, f, props.note);\n try {\n await navigator.clipboard.writeText(out);\n fallback.value = '';\n toast('ok', `Copied ${exportable.value} as ${FORMAT_LABEL[f]}.`);\n } catch {\n // Not an error worth a red banner: the text is right here, it just could not be put on the\n // clipboard for you.\n fallback.value = out;\n toast('error', 'Could not reach the clipboard — select the text below and copy it.');\n }\n}\n\nfunction apply(mode: 'replace' | 'merge'): void {\n emit('apply', usable.value, mode);\n text.value = '';\n open.value = false;\n}\n\nfunction selectAll(e: FocusEvent): void {\n (e.target as HTMLTextAreaElement).select();\n}\n</script>\n\n<template>\n <div class=\"vario\">\n <div class=\"row\">\n <span class=\"mute vario-lbl\">Copy as</span>\n <button\n v-for=\"f in VAR_FORMATS\"\n :key=\"f\"\n type=\"button\"\n class=\"sm ghost\"\n :disabled=\"!exportable\"\n :title=\"exportable ? `Copy all ${exportable} as ${FORMAT_LABEL[f]}` : 'There is nothing to copy yet'\"\n @click=\"copy(f)\"\n >\n {{ FORMAT_LABEL[f] }}\n </button>\n <span class=\"sep\" aria-hidden=\"true\" />\n <button type=\"button\" class=\"sm\" :aria-expanded=\"open\" @click=\"open = !open\">\n {{ open ? 'Close' : 'Paste to fill' }}\n </button>\n </div>\n\n <div v-if=\"fallback\" class=\"vario-box\">\n <p class=\"hint\">\n The clipboard could not be reached — a browser grants it only on a secure origin, and not\n always then. Select this and copy it by hand.\n </p>\n <textarea\n :value=\"fallback\"\n readonly\n rows=\"6\"\n spellcheck=\"false\"\n aria-label=\"the exported variables\"\n @focus=\"selectAll\"\n />\n <button type=\"button\" class=\"sm ghost\" @click=\"fallback = ''\">Hide</button>\n </div>\n\n <div v-if=\"open\" class=\"vario-box\">\n <slot />\n\n <textarea\n v-model=\"text\"\n rows=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n aria-label=\"paste variables\"\n placeholder=\"PR=7 REGION=eu-central …or two columns of CSV or TSV\"\n />\n\n <div class=\"row\" style=\"margin-top: var(--s2)\">\n <SelectMenu v-model=\"fmt\" label=\"how to read the paste\" :options=\"formatOptions\" />\n <span v-if=\"parsed\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n read as {{ FORMAT_LABEL[parsed.format] }}\n </span>\n </div>\n\n <template v-if=\"parsed\">\n <p class=\"hint\">\n <b>{{ usable.length }}</b> variable{{ usable.length === 1 ? '' : 's' }} read.\n <template v-if=\"skipped.length\">\n {{ skipped.length }} ({{ skipped.map((e) => e.k).join(', ') }}) had no value and will be\n skipped — a value stored here is never blank.\n </template>\n </p>\n\n <ul v-if=\"usable.length\" class=\"kvlist vario-preview\">\n <li v-for=\"e in usable.slice(0, PREVIEW)\" :key=\"e.k\">\n <span class=\"k\"><code>{{ e.k }}</code></span>\n <span class=\"v mono\">{{ e.v }}</span>\n </li>\n <li v-if=\"usable.length > PREVIEW\" class=\"mute\">\n and {{ usable.length - PREVIEW }} more\n </li>\n </ul>\n\n <!--\n Never a silent drop. A line that did not become a variable is shown with its number and\n its text, so the operator can see whether the paste was misread or the source was wrong.\n -->\n <div v-if=\"parsed.problems.length\" class=\"banner warn\">\n <b>\n {{ parsed.problems.length }} line{{ parsed.problems.length === 1 ? '' : 's' }} could not\n be read\n </b>\n <ul class=\"vario-problems\">\n <li v-for=\"p in parsed.problems\" :key=\"p.line\">\n <span class=\"mute\">line {{ p.line }}</span>\n <code>{{ p.text }}</code>\n <span class=\"dim\">{{ p.reason }}</span>\n </li>\n </ul>\n <p>\n They are not imported. Fix them and paste again, or apply what parsed and add the rest\n by hand.\n </p>\n </div>\n\n <div class=\"row\" style=\"margin-top: var(--s3)\">\n <button\n v-if=\"replace\"\n type=\"button\"\n class=\"primary\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Discard the current list and use exactly what was pasted' : 'Nothing parsed to apply'\"\n @click=\"apply('replace')\"\n >\n Replace all\n </button>\n <button\n type=\"button\"\n :class=\"replace ? '' : 'primary'\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Overwrite matching names, keep the rest, add what is new' : 'Nothing parsed to apply'\"\n @click=\"apply('merge')\"\n >\n Merge\n </button>\n <button type=\"button\" class=\"ghost\" @click=\"open = false\">Cancel</button>\n </div>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.vario {\n margin-top: var(--s3);\n}\n.vario-lbl {\n font-size: var(--t-xs);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n/* Separates two groups of controls that do opposite things; the gap alone read as one strip. */\n.sep {\n width: 1px;\n height: 18px;\n background: var(--line);\n margin-inline: var(--s1);\n}\n.vario-box {\n margin-top: var(--s3);\n padding: var(--s3);\n border: 1px solid var(--line);\n border-radius: var(--r2);\n max-width: 640px;\n}\n.vario-box > textarea {\n width: 100%;\n}\n/* A pasted value can hold newlines; collapsing them here would hide half of what is being applied. */\n.vario-preview {\n margin-top: var(--s2);\n}\n.vario-preview .v {\n white-space: pre-wrap;\n}\n.vario-problems {\n margin: var(--s2) 0 0;\n padding: 0;\n list-style: none;\n display: grid;\n gap: var(--s1);\n font-size: var(--t-sm);\n}\n/*\n * Two columns, not a wrapping row: a long reason wrapping under `line 5` read as a new paragraph of\n * the banner rather than as that line's explanation.\n */\n.vario-problems > li {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 2px var(--s2);\n align-items: baseline;\n}\n.vario-problems > li > code {\n justify-self: start;\n}\n.vario-problems > li > .dim {\n grid-column: 2;\n}\n</style>\n"],"names":["VAR_FORMATS","FORMAT_LABEL","NAME","PLAIN","DELIM","HEADER_NAME","HEADER_VALUE","formatVars","pairs","format","note","rows","e","l","envQuote","d","f","v","delimQuote","parseVars","text","detectFormat","parseEnv","parseDelimited","first","tsv","score","csv","s","r","splitRows","padOrTrim","lines","problems","i","raw","at","eq","k","nameReason","rest","q","start","body","end","closingQuote","inner","unescape","lastWins","ESCAPES","out","c","fields","a","b","n","field","quoted","inQuote","line","rowLine","rowStart","endField","m","mergeVars","existing","incoming","next","hit","PREVIEW","props","__props","emit","__emit","open","ref","fmt","fallback","exportable","computed","parsed","skipped","usable","formatOptions","copy","toast","apply","mode","selectAll","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_Fragment","_renderList","_unref","$event","_toDisplayString","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_7","_renderSlot","_ctx","_hoisted_8","_createVNode","SelectMenu","_hoisted_9","_hoisted_10","_createTextVNode","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","p","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VarIO-CNugbzeb.js","sources":["../../src/composables/useVarFormats.ts","../../src/components/VarIO.vue"],"sourcesContent":["/**\n * `.env`, CSV and TSV for variable lists — the one implementation both variable surfaces share.\n *\n * WHY PURE FUNCTIONS IN A `.ts`. There is no component test harness in this app, so anything that\n * can be wrong in an interesting way has to live somewhere `bun test` can reach it. Everything here\n * is a string in and a value out; the components own the chrome and the policy, and nothing else.\n *\n * THE ROUND-TRIP IS THE CONTRACT. `parseVars(formatVars(pairs, f), f)` returns `pairs` for every\n * value the editors can hold — newlines, quotes, `#`, leading and trailing spaces, backslashes,\n * `=`, the empty string. That is what makes \"copy them out, paste them back\" safe, and it is what\n * `test/useVarFormats.test.ts` spends most of its lines on. Two deliberate exceptions, both tested:\n *\n * - A lone carriage return survives `.env` (escaped as `\\r`) but not CSV/TSV, where CRLF and CR\n * are normalised to LF before parsing — a row terminator cannot also be data without a\n * dialect flag, and no `<input type=\"text\">` can produce one anyway.\n * - `parseVars` collapses a repeated name to its last value, as every dotenv reader does. The\n * import preview shows the result, so the collapse is visible before anything is replaced.\n *\n * WHY CSV AND TSV ALWAYS EXPORT A `name,value` HEADER. A header row is ambiguous with data — a\n * variable really can be called `name` — and guessing wrong either eats a variable or imports a\n * label. Emitting the header makes our own output unambiguous: the header we wrote is the row the\n * detector eats, so `{ k: 'name', v: 'value' }` round-trips. Foreign input whose first row happens\n * to read `name,value` loses that row, which is why the importer previews what it parsed.\n *\n * A NAME MUST BE ONE A SPEC CAN REFERENCE. `VarEditor` only trims what is typed, so `foo-bar` can\n * exist in the editor and be exported — but `${foo-bar}` is not a thing `internal/spec` can\n * interpolate, so importing it would add a row that can never do anything. It comes back as a\n * problem instead, named, where the operator can see it.\n *\n * A MALFORMED LINE IS NEVER DROPPED SILENTLY. Every line that does not become a pair comes back in\n * `problems` with its line number, its text and why — a paste that half-worked without saying so\n * is how a deployment ends up missing the one variable its stack name is built from.\n */\n\nimport type { VarPair } from '../api/types';\n\nexport type VarFormat = 'env' | 'csv' | 'tsv';\n\n/** A line that did not become a pair. Reported, never swallowed. */\nexport type VarProblem = { line: number; text: string; reason: string };\n\nexport type VarParse = { format: VarFormat; pairs: VarPair[]; problems: VarProblem[] };\n\nexport const VAR_FORMATS: VarFormat[] = ['env', 'csv', 'tsv'];\nexport const FORMAT_LABEL: Record<VarFormat, string> = { env: '.env', csv: 'CSV', tsv: 'TSV' };\n\n/** The charset a spec can reference — `internal/spec`'s `${NAME}` regex, not a UI opinion. */\nconst NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;\n/** Values needing no quotes: bytes that dotenv, compose and a shell all read back identically. */\nconst PLAIN = /^[A-Za-z0-9_@%+=:,./-]+$/;\nconst DELIM: Record<'csv' | 'tsv', string> = { csv: ',', tsv: '\\t' };\nconst HEADER_NAME = new Set(['name', 'key', 'variable', 'var']);\nconst HEADER_VALUE = new Set(['value', 'val']);\n\n// ── export ────────────────────────────────────────────────────────────────────────────────────────\n\n/**\n * Render pairs. `note` becomes `#` comment lines and is only expressible in `.env` — the surface\n * that passes one (host secrets, whose values are withheld) also says so on the page, because CSV\n * has nowhere to put it.\n */\nexport function formatVars(pairs: VarPair[], format: VarFormat, note?: string): string {\n const rows = pairs.filter((e) => e.k);\n if (format === 'env') {\n const head = note ? note.split('\\n').map((l) => `# ${l}`) : [];\n return [...head, ...rows.map((e) => `${e.k}=${envQuote(e.v)}`)].join('\\n') + '\\n';\n }\n const d = DELIM[format];\n return [['name', 'value'], ...rows.map((e) => [e.k, e.v])]\n .map((f) => f.map((v) => delimQuote(v ?? '', d)).join(d))\n .join('\\n') + '\\n';\n}\n\nfunction envQuote(v: string): string {\n // An empty value stays bare: `KEY=` parses back to `''` and reads as \"fill this in\", which is\n // exactly what a withheld secret is.\n if (v === '' || PLAIN.test(v)) return v;\n return `\"${v.replace(/[\\\\\"]/g, '\\\\$&').replace(/\\n/g, '\\\\n').replace(/\\r/g, '\\\\r')}\"`;\n}\n\nfunction delimQuote(v: string, d: string): string {\n const safe =\n v === '' || (!v.includes(d) && !v.includes('\"') && !/[\\n\\r]/.test(v) && v === v.trim());\n return safe ? v : `\"${v.replace(/\"/g, '\"\"')}\"`;\n}\n\n// ── import ────────────────────────────────────────────────────────────────────────────────────────\n\n/** Parse in `format`, or in whatever `detectFormat` reads the text as. */\nexport function parseVars(text: string, format?: VarFormat): VarParse {\n const f = format ?? detectFormat(text);\n return f === 'env' ? parseEnv(text) : parseDelimited(text, f);\n}\n\n/**\n * Which format is this? The first meaningful line decides `.env`; otherwise both delimiters are\n * tried with the real splitter and scored, because `indexOf('\\t')` calls a CSV whose value contains\n * a tab a TSV. A field that still holds a `\"` after splitting is the tell that the row was cut on\n * the wrong delimiter, so it counts against that reading.\n */\nexport function detectFormat(text: string): VarFormat {\n const first = text.split(/\\r\\n|\\n|\\r/).find((l) => l.trim() && !l.trim().startsWith('#')) ?? '';\n if (/^(export\\s+)?[A-Za-z_][A-Za-z0-9_]*\\s*=/.test(first.trim())) return 'env';\n const tsv = score(text, '\\t');\n const csv = score(text, ',');\n if (tsv <= 0 && csv <= 0) return 'env';\n return tsv > csv ? 'tsv' : 'csv';\n}\n\nfunction score(text: string, d: string): number {\n let s = 0;\n for (const r of splitRows(text, d)) {\n const f = padOrTrim(r.fields);\n if (!f.length) continue;\n s += f.length === 2 ? 1 : -1;\n for (const v of f) if (v.includes('\"')) s -= 1;\n }\n return s;\n}\n\nfunction parseEnv(text: string): VarParse {\n const lines = text.split(/\\r\\n|\\n|\\r/);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n\n for (let i = 0; i < lines.length; i++) {\n const raw = lines[i] ?? '';\n const at = i + 1;\n if (!raw.trim() || raw.trim().startsWith('#')) continue;\n\n // Only the LEFT is trimmed: a quoted value that spans lines keeps the spaces before its first\n // newline, which `raw.trim()` would have eaten.\n const s = raw.trimStart().replace(/^export\\s+/, '');\n const eq = s.indexOf('='); // the FIRST `=`; the rest of the line is value, `=` signs and all\n if (eq < 0) {\n problems.push({ line: at, text: raw, reason: 'no \"=\" — a line reads NAME=value' });\n continue;\n }\n const k = s.slice(0, eq).trim();\n if (!NAME.test(k)) {\n problems.push({ line: at, text: raw, reason: nameReason(k) });\n continue;\n }\n\n const rest = s.slice(eq + 1).replace(/^[ \\t]+/, '');\n const q = rest[0];\n if (q === '\"' || q === \"'\") {\n const start = i;\n let body = rest.slice(1);\n let end = closingQuote(body, q);\n while (end < 0 && i + 1 < lines.length) {\n i++;\n body += '\\n' + (lines[i] ?? '');\n end = closingQuote(body, q);\n }\n if (end < 0) {\n // Rewind. The lines swallowed hunting for a closing quote are other people's lines, and\n // eating the rest of the file behind one stray `\"` is the silent drop this reports instead.\n i = start;\n problems.push({ line: at, text: raw, reason: 'the quote is never closed' });\n continue;\n }\n const inner = body.slice(0, end);\n // Single quotes are literal, double quotes carry escapes — dotenv's rule, and the one our\n // own exporter writes against. Anything after the closing quote is a trailing comment.\n pairs.push({ k, v: q === '\"' ? unescape(inner) : inner });\n continue;\n }\n // Unquoted: a ` #` starts a comment, trailing whitespace is not value.\n pairs.push({ k, v: rest.replace(/\\s+#.*$/, '').trimEnd() });\n }\n return { format: 'env', pairs: lastWins(pairs), problems };\n}\n\nconst ESCAPES: Record<string, string> = {\n n: '\\n',\n r: '\\r',\n t: '\\t',\n '\\\\': '\\\\',\n '\"': '\"',\n \"'\": \"'\",\n $: '$',\n '`': '`',\n};\n\nfunction unescape(s: string): string {\n let out = '';\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (c !== '\\\\' || i === s.length - 1) {\n out += c;\n continue;\n }\n const n = s[++i] ?? '';\n // An unknown escape keeps both characters: `C:\\path` typed inside quotes stays `C:\\path`\n // rather than losing the backslash to a rule nobody wrote down.\n out += ESCAPES[n] ?? '\\\\' + n;\n }\n return out;\n}\n\nfunction closingQuote(s: string, q: string): number {\n for (let i = 0; i < s.length; i++) {\n const c = s[i];\n if (q === '\"' && c === '\\\\') {\n i++;\n continue;\n }\n if (c === q) return i;\n }\n return -1;\n}\n\nfunction parseDelimited(text: string, format: 'csv' | 'tsv'): VarParse {\n const rows = splitRows(text, DELIM[format]);\n const pairs: VarPair[] = [];\n const problems: VarProblem[] = [];\n let first = true;\n\n for (const r of rows) {\n const fields = padOrTrim(r.fields);\n if (!fields.length) continue; // a blank line is not malformed\n if (first) {\n first = false;\n const a = (fields[0] ?? '').toLowerCase();\n const b = (fields[1] ?? '').toLowerCase();\n if (fields.length === 2 && HEADER_NAME.has(a) && HEADER_VALUE.has(b)) continue;\n }\n if (fields.length !== 2) {\n const n = fields.length;\n problems.push({\n line: r.line,\n text: r.raw,\n reason: `${n} column${n === 1 ? '' : 's'} — expected two, a name and a value`,\n });\n continue;\n }\n const k = (fields[0] ?? '').trim();\n if (!NAME.test(k)) {\n problems.push({ line: r.line, text: r.raw, reason: nameReason(k) });\n continue;\n }\n pairs.push({ k, v: fields[1] ?? '' });\n }\n return { format, pairs: lastWins(pairs), problems };\n}\n\ntype Row = { fields: string[]; line: number; raw: string };\n\n/**\n * RFC 4180 with one delimiter parameter: `\"\"` is a literal quote, a quoted field may hold the\n * delimiter and newlines, an unquoted field is trimmed. CRLF and lone CR are normalised to LF\n * first — a row terminator cannot also be data.\n */\nfunction splitRows(text: string, d: string): Row[] {\n const s = text.replace(/\\r\\n?/g, '\\n');\n const rows: Row[] = [];\n let fields: string[] = [];\n let field = '';\n let quoted = false;\n let inQuote = false;\n let line = 1;\n let rowLine = 1;\n let rowStart = 0;\n\n const endField = (): void => {\n fields.push(quoted ? field : field.trim());\n field = '';\n quoted = false;\n };\n\n for (let i = 0; i < s.length; i++) {\n const c = s[i] ?? '';\n if (inQuote) {\n if (c === '\"') {\n if (s[i + 1] === '\"') {\n field += '\"';\n i++;\n } else inQuote = false;\n } else {\n if (c === '\\n') line++;\n field += c;\n }\n continue;\n }\n // A quote opens a field only at its start — spaces before it are formatting, a quote after\n // real characters is data.\n if (c === '\"' && !quoted && field.trim() === '') {\n field = '';\n inQuote = true;\n quoted = true;\n continue;\n }\n if (c === d) {\n endField();\n continue;\n }\n if (c === '\\n') {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart, i) });\n fields = [];\n line++;\n rowLine = line;\n rowStart = i + 1;\n continue;\n }\n field += c;\n }\n if (field !== '' || quoted || fields.length) {\n endField();\n rows.push({ fields, line: rowLine, raw: s.slice(rowStart) });\n }\n return rows;\n}\n\n/**\n * Drop trailing empty columns a spreadsheet padded the row with — but never below two, or\n * `SECRET,` (a name whose value was withheld) would read as a one-column row instead of an empty\n * value. An all-empty row is a blank line.\n */\nfunction padOrTrim(fields: string[]): string[] {\n let n = fields.length;\n while (n > 0 && (fields[n - 1] ?? '') === '') n--;\n return n === 0 ? [] : fields.slice(0, Math.max(n, Math.min(2, fields.length)));\n}\n\nfunction nameReason(k: string): string {\n return k\n ? `\"${k}\" is not a usable variable name — letters, digits and _, not starting with a digit`\n : 'no name before the value';\n}\n\n/** dotenv semantics: a repeated name keeps its last value, at the position it first appeared. */\nfunction lastWins(pairs: VarPair[]): VarPair[] {\n const m = new Map<string, string>();\n for (const e of pairs) m.set(e.k, e.v);\n return [...m].map(([k, v]) => ({ k, v }));\n}\n\n/** Incoming overrides by name, everything else keeps its place. Blank editor rows are dropped. */\nexport function mergeVars(existing: VarPair[], incoming: VarPair[]): VarPair[] {\n const next = existing.filter((e) => e.k).map((e) => ({ ...e }));\n for (const e of incoming) {\n const hit = next.find((n) => n.k === e.k);\n if (hit) hit.v = e.v;\n else next.push({ ...e });\n }\n return next;\n}\n","<script setup lang=\"ts\">\n/**\n * Copy a variable list out as `.env`, CSV or TSV; paste one back in. The chrome only — every rule\n * about what those formats mean lives in `composables/useVarFormats.ts`, which is tested directly\n * because this component cannot be.\n *\n * NOTHING IS REPLACED BEFORE IT IS SHOWN. The paste box parses as you type and renders what it\n * read, including the lines it could not read, and only then offers the buttons that change\n * anything. A paste that half-worked in silence is how a deployment loses the one variable its\n * stack name is built from — which is the leak this project exists to prevent, arriving through a\n * text box.\n *\n * MERGE AND REPLACE ARE BOTH REAL ANSWERS, so the operator picks. `replace` is offered only where\n * this component's owner can actually replace the whole list: on the host-variable page a\n * \"replace\" would have to DELETE the rows the paste omits, and a delete that happens because\n * something was missing from a paste is not a thing to offer behind a two-word button.\n *\n * THE CLIPBOARD IS NOT ALWAYS THERE. `navigator.clipboard` is undefined on a plain-http origin —\n * exactly how this app is reached through a loopback tunnel — and a secure origin can still refuse\n * the write. A failed copy therefore reveals a selected textarea instead of only apologising in a\n * toast: the text exists either way, only the shortcut is missing.\n */\nimport { computed, ref } from 'vue';\nimport type { VarPair } from '../api/types';\nimport {\n FORMAT_LABEL,\n VAR_FORMATS,\n formatVars,\n parseVars,\n type VarFormat,\n} from '../composables/useVarFormats';\nimport { toast } from '../composables/useToasts';\nimport SelectMenu from './SelectMenu.vue';\n\nconst props = defineProps<{\n /** What \"copy\" exports. Blank rows are dropped by the formatter. */\n pairs: VarPair[];\n /** `#` comment lines on the `.env` export — the withheld-secrets surface passes one. */\n note?: string;\n /** Offer \"Replace all\". See the header: not every owner can honestly replace. */\n replace?: boolean;\n /** Skip parsed entries with no value, and say so. For a store that refuses an empty value. */\n skipEmpty?: boolean;\n /** An import is in flight upstream. */\n busy?: boolean;\n}>();\n\nconst emit = defineEmits<{ apply: [pairs: VarPair[], mode: 'replace' | 'merge'] }>();\n\nconst open = ref(false);\nconst text = ref('');\nconst fmt = ref('auto');\n/** The export text, revealed only when the clipboard refused it. */\nconst fallback = ref('');\n\nconst PREVIEW = 12;\n\nconst exportable = computed(() => props.pairs.filter((e) => e.k).length);\n\nconst parsed = computed(() =>\n text.value.trim()\n ? parseVars(text.value, fmt.value === 'auto' ? undefined : (fmt.value as VarFormat))\n : null,\n);\nconst skipped = computed(() =>\n props.skipEmpty && parsed.value ? parsed.value.pairs.filter((e) => !e.v) : [],\n);\nconst usable = computed(() =>\n parsed.value ? (props.skipEmpty ? parsed.value.pairs.filter((e) => e.v) : parsed.value.pairs) : [],\n);\n\nconst formatOptions = [\n { value: 'auto', label: 'Detect the format', hint: 'read the paste and decide' },\n ...VAR_FORMATS.map((f) => ({ value: f, label: `Read as ${FORMAT_LABEL[f]}` })),\n];\n\nasync function copy(f: VarFormat): Promise<void> {\n const out = formatVars(props.pairs, f, props.note);\n try {\n await navigator.clipboard.writeText(out);\n fallback.value = '';\n toast('ok', `Copied ${exportable.value} as ${FORMAT_LABEL[f]}.`);\n } catch {\n // Not an error worth a red banner: the text is right here, it just could not be put on the\n // clipboard for you.\n fallback.value = out;\n toast('error', 'Could not reach the clipboard — select the text below and copy it.');\n }\n}\n\nfunction apply(mode: 'replace' | 'merge'): void {\n emit('apply', usable.value, mode);\n text.value = '';\n open.value = false;\n}\n\nfunction selectAll(e: FocusEvent): void {\n (e.target as HTMLTextAreaElement).select();\n}\n</script>\n\n<template>\n <div class=\"vario\">\n <div class=\"row\">\n <span class=\"mute vario-lbl\">Copy as</span>\n <button\n v-for=\"f in VAR_FORMATS\"\n :key=\"f\"\n type=\"button\"\n class=\"sm ghost\"\n :disabled=\"!exportable\"\n :title=\"exportable ? `Copy all ${exportable} as ${FORMAT_LABEL[f]}` : 'There is nothing to copy yet'\"\n @click=\"copy(f)\"\n >\n {{ FORMAT_LABEL[f] }}\n </button>\n <span class=\"sep\" aria-hidden=\"true\" />\n <button type=\"button\" class=\"sm\" :aria-expanded=\"open\" @click=\"open = !open\">\n {{ open ? 'Close' : 'Paste to fill' }}\n </button>\n </div>\n\n <div v-if=\"fallback\" class=\"vario-box\">\n <p class=\"hint\">\n The clipboard could not be reached — a browser grants it only on a secure origin, and not\n always then. Select this and copy it by hand.\n </p>\n <textarea\n :value=\"fallback\"\n readonly\n rows=\"6\"\n spellcheck=\"false\"\n aria-label=\"the exported variables\"\n @focus=\"selectAll\"\n />\n <button type=\"button\" class=\"sm ghost\" @click=\"fallback = ''\">Hide</button>\n </div>\n\n <div v-if=\"open\" class=\"vario-box\">\n <slot />\n\n <textarea\n v-model=\"text\"\n rows=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n aria-label=\"paste variables\"\n placeholder=\"PR=7 REGION=eu-central …or two columns of CSV or TSV\"\n />\n\n <div class=\"row\" style=\"margin-top: var(--s2)\">\n <SelectMenu v-model=\"fmt\" label=\"how to read the paste\" :options=\"formatOptions\" />\n <span v-if=\"parsed\" class=\"mute\" style=\"font-size: var(--t-sm)\">\n read as {{ FORMAT_LABEL[parsed.format] }}\n </span>\n </div>\n\n <template v-if=\"parsed\">\n <p class=\"hint\">\n <b>{{ usable.length }}</b> variable{{ usable.length === 1 ? '' : 's' }} read.\n <template v-if=\"skipped.length\">\n {{ skipped.length }} ({{ skipped.map((e) => e.k).join(', ') }}) had no value and will be\n skipped — a value stored here is never blank.\n </template>\n </p>\n\n <ul v-if=\"usable.length\" class=\"kvlist vario-preview\">\n <li v-for=\"e in usable.slice(0, PREVIEW)\" :key=\"e.k\">\n <span class=\"k\"><code>{{ e.k }}</code></span>\n <span class=\"v mono\">{{ e.v }}</span>\n </li>\n <li v-if=\"usable.length > PREVIEW\" class=\"mute\">\n and {{ usable.length - PREVIEW }} more\n </li>\n </ul>\n\n <!--\n Never a silent drop. A line that did not become a variable is shown with its number and\n its text, so the operator can see whether the paste was misread or the source was wrong.\n -->\n <div v-if=\"parsed.problems.length\" class=\"banner warn\">\n <b>\n {{ parsed.problems.length }} line{{ parsed.problems.length === 1 ? '' : 's' }} could not\n be read\n </b>\n <ul class=\"vario-problems\">\n <li v-for=\"p in parsed.problems\" :key=\"p.line\">\n <span class=\"mute\">line {{ p.line }}</span>\n <code>{{ p.text }}</code>\n <span class=\"dim\">{{ p.reason }}</span>\n </li>\n </ul>\n <p>\n They are not imported. Fix them and paste again, or apply what parsed and add the rest\n by hand.\n </p>\n </div>\n\n <div class=\"row\" style=\"margin-top: var(--s3)\">\n <button\n v-if=\"replace\"\n type=\"button\"\n class=\"primary\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Discard the current list and use exactly what was pasted' : 'Nothing parsed to apply'\"\n @click=\"apply('replace')\"\n >\n Replace all\n </button>\n <button\n type=\"button\"\n :class=\"replace ? '' : 'primary'\"\n :disabled=\"!usable.length || busy\"\n :title=\"usable.length ? 'Overwrite matching names, keep the rest, add what is new' : 'Nothing parsed to apply'\"\n @click=\"apply('merge')\"\n >\n Merge\n </button>\n <button type=\"button\" class=\"ghost\" @click=\"open = false\">Cancel</button>\n </div>\n </template>\n </div>\n </div>\n</template>\n\n<style scoped>\n.vario {\n margin-top: var(--s3);\n}\n.vario-lbl {\n font-size: var(--t-xs);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n}\n/* Separates two groups of controls that do opposite things; the gap alone read as one strip. */\n.sep {\n width: 1px;\n height: 18px;\n background: var(--line);\n margin-inline: var(--s1);\n}\n.vario-box {\n margin-top: var(--s3);\n padding: var(--s3);\n border: 1px solid var(--line);\n border-radius: var(--r2);\n max-width: 640px;\n}\n.vario-box > textarea {\n width: 100%;\n}\n/* A pasted value can hold newlines; collapsing them here would hide half of what is being applied. */\n.vario-preview {\n margin-top: var(--s2);\n}\n.vario-preview .v {\n white-space: pre-wrap;\n}\n.vario-problems {\n margin: var(--s2) 0 0;\n padding: 0;\n list-style: none;\n display: grid;\n gap: var(--s1);\n font-size: var(--t-sm);\n}\n/*\n * Two columns, not a wrapping row: a long reason wrapping under `line 5` read as a new paragraph of\n * the banner rather than as that line's explanation.\n */\n.vario-problems > li {\n display: grid;\n grid-template-columns: auto minmax(0, 1fr);\n gap: 2px var(--s2);\n align-items: baseline;\n}\n.vario-problems > li > code {\n justify-self: start;\n}\n.vario-problems > li > .dim {\n grid-column: 2;\n}\n</style>\n"],"names":["VAR_FORMATS","FORMAT_LABEL","NAME","PLAIN","DELIM","HEADER_NAME","HEADER_VALUE","formatVars","pairs","format","note","rows","e","l","envQuote","d","f","v","delimQuote","parseVars","text","detectFormat","parseEnv","parseDelimited","first","tsv","score","csv","s","r","splitRows","padOrTrim","lines","problems","i","raw","at","eq","k","nameReason","rest","q","start","body","end","closingQuote","inner","unescape","lastWins","ESCAPES","out","c","fields","a","b","n","field","quoted","inQuote","line","rowLine","rowStart","endField","m","mergeVars","existing","incoming","next","hit","PREVIEW","props","__props","emit","__emit","open","ref","fmt","fallback","exportable","computed","parsed","skipped","usable","formatOptions","copy","toast","apply","mode","selectAll","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_Fragment","_renderList","_unref","$event","_toDisplayString","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_7","_renderSlot","_ctx","_hoisted_8","_createVNode","SelectMenu","_hoisted_9","_hoisted_10","_createTextVNode","_hoisted_11","_hoisted_12","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","p","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{d as K,c as r,a as e,e as c,z as S,f,F as N,t as i,r as I,h as $,n as v,w as Q,g as R,v as X,al as Y,L as ee,i as p,j as b,p as A,C as U,o,a6 as M}from"./index-
|
|
2
|
-
//# sourceMappingURL=VariablesView-
|
|
1
|
+
import{d as K,c as r,a as e,e as c,z as S,f,F as N,t as i,r as I,h as $,n as v,w as Q,g as R,v as X,al as Y,L as ee,i as p,j as b,p as A,C as U,o,a6 as M}from"./index-DlhYSSKq.js";import{A as L}from"./ActionButton-BaGfzZKv.js";import{E as te}from"./EquivalentCommand-CAhOGrAR.js";import{_ as ae}from"./ErrorNote.vue_vue_type_script_setup_true_lang-DU01Dqor.js";import{_ as P}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 se}from"./RefreshButton.vue_vue_type_script_setup_true_lang-Dk-lSii3.js";import{V as O}from"./VarIO-CNugbzeb.js";import{_ as le}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./SelectMenu.vue_vue_type_script_setup_true_lang-PIaLSNsk.js";const oe={class:"page-head"},ne={key:1,class:"panel"},re={class:"grid-2"},ie={class:"panel"},ue={class:"phead"},de={class:"mute",style:{"font-size":"var(--t-xs)"}},ve={key:1,role:"table",class:"cards"},ce={role:"rowgroup"},pe={role:"cell",class:"name","data-label":"name"},me={role:"cell",class:"break","data-label":"value"},he={role:"cell",class:"dim nowrap","data-label":"updated"},fe={role:"cell",class:"right nowrap","data-label":""},be=["onClick"],_e={key:2,class:"hint"},ye={class:"panel"},ge={class:"phead"},ke={class:"mute",style:{"font-size":"var(--t-xs)"}},we={key:1,role:"table",class:"cards"},$e={role:"rowgroup"},Ae={role:"cell",class:"name","data-label":"name"},Ve={role:"cell",class:"dim nowrap","data-label":"updated"},xe={role:"cell",class:"right nowrap","data-label":""},Ce=["onClick"],De={key:2,class:"hint"},Ee={class:"panel"},Se={class:"section",style:{"margin-bottom":"var(--s3)"}},Ne={class:"row wrap two-up"},Re={class:"field grow"},Ue=["disabled","aria-invalid"],Me={class:"field grow"},ze=["type","placeholder"],Be={key:0,id:"var-name-err",class:"s-failed",style:{"margin-top":"var(--s2)"}},Te={class:"row",style:{"margin-top":"var(--s3)"}},Fe={class:"check"},Ie=["disabled"],Le={key:1,class:"hint"},Pe={class:"row",style:{"margin-top":"var(--s4)"}},He=["disabled"],Oe={key:1,class:"mute",style:{"font-size":"var(--t-sm)"}},je="Values are never exported. Fill each one in.",We=K({__name:"VariablesView",setup(Ze){const V=p([]),x=p(!1),d=p(""),z=p(!1),_=b(()=>V.value.filter(a=>!a.secret)),y=b(()=>V.value.filter(a=>a.secret)),j=b(()=>_.value.map(a=>({k:a.name,v:a.value??""}))),W=b(()=>y.value.map(a=>({k:a.name,v:""}))),s=p({name:"",value:"",secret:!1}),C=p(!1),g=p(!1),n=p(null),D=/^[A-Za-z_][A-Za-z0-9_]*$/,Z=b(()=>!C.value&&D.test(s.value.name)&&s.value.value.length>0);async function h(){const a=await A.get("/api/host-vars");if(x.value=!0,a.status===404){z.value=!0;return}if(!a.ok){d.value=U(a,"load the variables");return}d.value="",V.value=a.body.entries??[]}h();function B(a){n.value=a,s.value={name:a.name,value:a.value??"",secret:a.secret}}function E(){n.value=null,s.value={name:"",value:"",secret:!1}}async function q(){C.value=!0;const a=await A.put(`/api/host-vars/${s.value.name}`,{value:s.value.value,secret:s.value.secret});if(C.value=!1,!a.ok){d.value=U(a,`store ${s.value.name}`);return}d.value="",M("ok",`${n.value?"Saved":"Added"} ${s.value.name}.`),E(),h()}async function T(a,t){g.value=!0;const u=[];for(const l of t)(await A.put(`/api/host-vars/${encodeURIComponent(l.k)}`,{value:l.v,secret:a})).ok||u.push(l.k);g.value=!1;const k=a?"secret":"variable",m=t.length-u.length;u.length?d.value=`Stored ${m} of ${t.length}. Failed: ${u.join(", ")}.`:(d.value="",M("ok",`Stored ${m} ${k}${m===1?"":"s"}.`)),h()}const G=a=>T(!1,a),J=a=>T(!0,a);async function F(a){var u;const t=await A.del(`/api/host-vars/${a.name}`);if(!t.ok){d.value=U(t,`delete ${a.name}`);return}M("ok",`Deleted ${a.name}. Specs using it will not resolve.`),((u=n.value)==null?void 0:u.name)===a.name&&E(),h()}return(a,t)=>{var u,k,m;return o(),r("div",null,[e("div",oe,[t[3]||(t[3]=e("div",null,[e("h1",null,"Variables & secrets"),e("div",{class:"sub"}," Host-level values every spec can reference. Deleting one stops the specs using it from resolving. ")],-1)),t[4]||(t[4]=e("span",{class:"grow"},null,-1)),c(se,{run:h})]),d.value?(o(),S(ae,{key:0,text:d.value,title:"Could not load the variables."},null,8,["text"])):f("",!0),z.value?(o(),r("section",ne,[...t[5]||(t[5]=[e("div",{class:"banner plain"},[e("b",null,"This server has no variable store."),e("p",null,"Upgrade the host to use these.")],-1)])])):(o(),r(N,{key:2},[e("div",re,[e("section",ie,[e("div",ue,[t[6]||(t[6]=e("h2",{class:"section"},"Variables",-1)),t[7]||(t[7]=e("span",{class:"grow"},null,-1)),e("span",de,i(_.value.length),1)]),x.value?_.value.length?(o(),r("table",ve,[t[9]||(t[9]=e("thead",{role:"rowgroup"},[e("tr",{role:"row"},[e("th",{role:"columnheader"},"Name"),e("th",{role:"columnheader"},"Value"),e("th",{role:"columnheader"},"Updated"),e("th",{role:"columnheader","aria-label":"Actions"})])],-1)),e("tbody",ce,[(o(!0),r(N,null,I(_.value,l=>(o(),r("tr",{key:l.name,role:"row"},[e("td",pe,[e("code",null,i(l.name),1)]),e("td",me,i(l.value),1),e("td",he,[c(P,{at:l.updatedAt},null,8,["at"])]),e("td",fe,[e("button",{class:"ghost sm",onClick:w=>B(l)},"Edit",8,be),c(L,{class:"danger sm",confirm:`Delete ${l.name}?`,onRun:w=>F(l)},{default:$(()=>[...t[8]||(t[8]=[v(" Delete ",-1)])]),_:1},8,["confirm","onRun"])])]))),128))])])):(o(),r("p",_e,"Add your first variable below.")):(o(),S(H,{key:0,rows:2,tall:""})),c(O,{pairs:j.value,busy:g.value,"skip-empty":"",onApply:G},{default:$(()=>[...t[10]||(t[10]=[e("p",{class:"hint",style:{"margin-top":"0"}},"Overwrites matching names. Deletes nothing.",-1)])]),_:1},8,["pairs","busy"])]),e("section",ye,[e("div",ge,[t[11]||(t[11]=e("h2",{class:"section"},"Secrets",-1)),t[12]||(t[12]=e("span",{class:"grow"},null,-1)),e("span",ke,i(y.value.length),1)]),x.value?y.value.length?(o(),r("table",we,[t[14]||(t[14]=e("thead",{role:"rowgroup"},[e("tr",{role:"row"},[e("th",{role:"columnheader"},"Name"),e("th",{role:"columnheader"},"Updated"),e("th",{role:"columnheader","aria-label":"Actions"})])],-1)),e("tbody",$e,[(o(!0),r(N,null,I(y.value,l=>(o(),r("tr",{key:l.name,role:"row"},[e("td",Ae,[e("code",null,i(l.name),1)]),e("td",Ve,[c(P,{at:l.updatedAt},null,8,["at"])]),e("td",xe,[e("button",{class:"ghost sm",onClick:w=>B(l)},"Replace",8,Ce),c(L,{class:"danger sm",confirm:`Delete ${l.name}?`,onRun:w=>F(l)},{default:$(()=>[...t[13]||(t[13]=[v(" Delete ",-1)])]),_:1},8,["confirm","onRun"])])]))),128))])])):(o(),r("p",De,"Add your first secret below.")):(o(),S(H,{key:0,rows:2,tall:""})),t[16]||(t[16]=e("p",{class:"hint"},[v("A copied list carries "),e("b",null,"names only"),v(" — values cannot be read back.")],-1)),c(O,{pairs:W.value,note:je,busy:g.value,"skip-empty":"",onApply:J},{default:$(()=>[...t[15]||(t[15]=[e("p",{class:"hint",style:{"margin-top":"0"}},[v(" Stored "),e("b",null,"write-only"),v(". Empty lines are skipped, never stored over a live secret. ")],-1)])]),_:1},8,["pairs","busy"])])]),e("section",Ee,[e("h2",Se,i(n.value?n.value.secret?`Replace the value of ${n.value.name}`:`Edit ${n.value.name}`:"Add a variable or secret"),1),e("form",{onSubmit:Q(q,["prevent"])},[e("div",Ne,[e("label",Re,[t[17]||(t[17]=e("span",null,"Name",-1)),R(e("input",{"onUpdate:modelValue":t[0]||(t[0]=l=>s.value.name=l),type:"text",placeholder:"DB_PASSWORD",spellcheck:"false",autocomplete:"off",disabled:!!n.value,"aria-describedby":"var-name-err","aria-invalid":!!s.value.name&&!D.test(s.value.name)},null,8,Ue),[[X,s.value.name,void 0,{trim:!0}]])]),e("label",Me,[t[18]||(t[18]=e("span",null,"Value",-1)),R(e("input",{"onUpdate:modelValue":t[1]||(t[1]=l=>s.value.value=l),type:s.value.secret?"password":"text",placeholder:(u=n.value)!=null&&u.secret?"enter the new value":"eu-central",spellcheck:"false",autocomplete:"off"},null,8,ze),[[Y,s.value.value]])])]),s.value.name&&!D.test(s.value.name)?(o(),r("p",Be," Letters, digits and _ only, not starting with a digit. ")):f("",!0),e("div",Te,[e("label",Fe,[R(e("input",{"onUpdate:modelValue":t[2]||(t[2]=l=>s.value.secret=l),type:"checkbox",disabled:((k=n.value)==null?void 0:k.secret)===!0},null,8,Ie),[[ee,s.value.secret]]),t[19]||(t[19]=v(" Secret — the value is never shown again ",-1))])]),(m=n.value)!=null&&m.secret?(o(),r("p",Le," A secret cannot become a readable variable. Delete it and add a variable instead. ")):f("",!0),e("div",Pe,[e("button",{class:"primary",type:"submit",disabled:!Z.value},i(n.value?"Save":"Add"),9,He),n.value?(o(),r("button",{key:0,type:"button",class:"ghost",onClick:E},"Cancel")):f("",!0),n.value?f("",!0):(o(),r("span",Oe,[t[20]||(t[20]=v(" Reference it as ",-1)),e("code",null,"$"+i("{")+i(s.value.secret?"secrets":"vars")+"."+i(s.value.name||"NAME")+i("}"),1),t[21]||(t[21]=v(" in any spec ",-1))]))]),c(te,{what:"storing this value",method:"PUT",path:`/api/host-vars/${s.value.name||"NAME"}`,body:{value:s.value.secret?"…":s.value.value||"…",secret:s.value.secret}},null,8,["path","body"])],32)])],64))])}}}),st=le(We,[["__scopeId","data-v-7be45b15"]]);export{st as default};
|
|
2
|
+
//# sourceMappingURL=VariablesView-qrqDNseI.js.map
|