@samyx/preview-stacks-ui 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/dist/assets/{ActionButton.vue_vue_type_script_setup_true_lang-CxT4O7XN.js → ActionButton.vue_vue_type_script_setup_true_lang-C6PZurL0.js} +2 -2
  2. package/dist/assets/{ActionButton.vue_vue_type_script_setup_true_lang-CxT4O7XN.js.map → ActionButton.vue_vue_type_script_setup_true_lang-C6PZurL0.js.map} +1 -1
  3. package/dist/assets/{AxesTab-BJZ9B5js.js → AxesTab-BNAORqpJ.js} +2 -2
  4. package/dist/assets/{AxesTab-BJZ9B5js.js.map → AxesTab-BNAORqpJ.js.map} +1 -1
  5. package/dist/assets/{ConfigTab-CKjpaZmg.js → ConfigTab-8Ifz-gnA.js} +2 -2
  6. package/dist/assets/{ConfigTab-CKjpaZmg.js.map → ConfigTab-8Ifz-gnA.js.map} +1 -1
  7. package/dist/assets/ConfigView-CPj8QeBo.js +2 -0
  8. package/dist/assets/ConfigView-CPj8QeBo.js.map +1 -0
  9. package/dist/assets/{ConflictNote.vue_vue_type_script_setup_true_lang-cZYd7gRK.js → ConflictNote.vue_vue_type_script_setup_true_lang-B0PJUns4.js} +2 -2
  10. package/dist/assets/{ConflictNote.vue_vue_type_script_setup_true_lang-cZYd7gRK.js.map → ConflictNote.vue_vue_type_script_setup_true_lang-B0PJUns4.js.map} +1 -1
  11. package/dist/assets/{DangerTab-XshYLxAZ.js → DangerTab-BwXkb6Rr.js} +2 -2
  12. package/dist/assets/{DangerTab-XshYLxAZ.js.map → DangerTab-BwXkb6Rr.js.map} +1 -1
  13. package/dist/assets/{DashboardView-DqIE-Aof.js → DashboardView-CuDBbXnZ.js} +2 -2
  14. package/dist/assets/{DashboardView-DqIE-Aof.js.map → DashboardView-CuDBbXnZ.js.map} +1 -1
  15. package/dist/assets/{DeploymentDetailView-nFBQCzq0.js → DeploymentDetailView-qtlOQQ0z.js} +2 -2
  16. package/dist/assets/{DeploymentDetailView-nFBQCzq0.js.map → DeploymentDetailView-qtlOQQ0z.js.map} +1 -1
  17. package/dist/assets/{DeploymentsView-CalAGK0U.js → DeploymentsView-CQwTTlTI.js} +2 -2
  18. package/dist/assets/{DeploymentsView-CalAGK0U.js.map → DeploymentsView-CQwTTlTI.js.map} +1 -1
  19. package/dist/assets/{EquivalentCommand-BjlPIrt9.js → EquivalentCommand-DX5xFW-p.js} +3 -3
  20. package/dist/assets/{EquivalentCommand-BjlPIrt9.js.map → EquivalentCommand-DX5xFW-p.js.map} +1 -1
  21. package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-B5-EJpIO.js → ErrorNote.vue_vue_type_script_setup_true_lang-CUZs06O0.js} +2 -2
  22. package/dist/assets/{ErrorNote.vue_vue_type_script_setup_true_lang-B5-EJpIO.js.map → ErrorNote.vue_vue_type_script_setup_true_lang-CUZs06O0.js.map} +1 -1
  23. package/dist/assets/{HelpModal.vue_vue_type_script_setup_true_lang-DWDQ-wIa.js → HelpModal.vue_vue_type_script_setup_true_lang-ErbSSi6D.js} +2 -2
  24. package/dist/assets/{HelpModal.vue_vue_type_script_setup_true_lang-DWDQ-wIa.js.map → HelpModal.vue_vue_type_script_setup_true_lang-ErbSSi6D.js.map} +1 -1
  25. package/dist/assets/{JobDetailView-BExTkW8o.js → JobDetailView-BgoEL7EB.js} +2 -2
  26. package/dist/assets/{JobDetailView-BExTkW8o.js.map → JobDetailView-BgoEL7EB.js.map} +1 -1
  27. package/dist/assets/{JobsView-h0PuFN8o.js → JobsView-CVG0iuN4.js} +2 -2
  28. package/dist/assets/{JobsView-h0PuFN8o.js.map → JobsView-CVG0iuN4.js.map} +1 -1
  29. package/dist/assets/{LogViewer.vue_vue_type_script_setup_true_lang-BNqCyGeQ.js → LogViewer.vue_vue_type_script_setup_true_lang-Sg8LzIyX.js} +3 -3
  30. package/dist/assets/{LogViewer.vue_vue_type_script_setup_true_lang-BNqCyGeQ.js.map → LogViewer.vue_vue_type_script_setup_true_lang-Sg8LzIyX.js.map} +1 -1
  31. package/dist/assets/LoginView-C90w1dZ2.css +1 -0
  32. package/dist/assets/LoginView-Gpnnyg1m.js +2 -0
  33. package/dist/assets/LoginView-Gpnnyg1m.js.map +1 -0
  34. package/dist/assets/{LogsTab-CKNkR_07.js → LogsTab-CZ0fNgsO.js} +2 -2
  35. package/dist/assets/{LogsTab-CKNkR_07.js.map → LogsTab-CZ0fNgsO.js.map} +1 -1
  36. package/dist/assets/{NotFoundView-B3377DMC.js → NotFoundView-BE2kvYuP.js} +2 -2
  37. package/dist/assets/{NotFoundView-B3377DMC.js.map → NotFoundView-BE2kvYuP.js.map} +1 -1
  38. package/dist/assets/{NotifiersView-BrPpuf5o.js → NotifiersView-Df_CM6Bj.js} +2 -2
  39. package/dist/assets/{NotifiersView-BrPpuf5o.js.map → NotifiersView-Df_CM6Bj.js.map} +1 -1
  40. package/dist/assets/{OverviewTab-DY-d5Ia3.js → OverviewTab-BSeic8dR.js} +2 -2
  41. package/dist/assets/{OverviewTab-DY-d5Ia3.js.map → OverviewTab-BSeic8dR.js.map} +1 -1
  42. package/dist/assets/{PublicLogsView-HBPDlV47.js → PublicLogsView-BDyvxkgj.js} +2 -2
  43. package/dist/assets/{PublicLogsView-HBPDlV47.js.map → PublicLogsView-BDyvxkgj.js.map} +1 -1
  44. package/dist/assets/{RefreshButton.vue_vue_type_script_setup_true_lang-C7qymrfD.js → RefreshButton.vue_vue_type_script_setup_true_lang-ZUc7pgOx.js} +2 -2
  45. package/dist/assets/{RefreshButton.vue_vue_type_script_setup_true_lang-C7qymrfD.js.map → RefreshButton.vue_vue_type_script_setup_true_lang-ZUc7pgOx.js.map} +1 -1
  46. package/dist/assets/{RegistriesView-B0XRfL2L.js → RegistriesView-DMzsLFcT.js} +2 -2
  47. package/dist/assets/{RegistriesView-B0XRfL2L.js.map → RegistriesView-DMzsLFcT.js.map} +1 -1
  48. package/dist/assets/RelativeTime.vue_vue_type_script_setup_true_lang-D4NO-r5D.js +2 -0
  49. package/dist/assets/{RelativeTime.vue_vue_type_script_setup_true_lang-BoNYQAdk.js.map → RelativeTime.vue_vue_type_script_setup_true_lang-D4NO-r5D.js.map} +1 -1
  50. package/dist/assets/{RequiresTab-CwaFcz5T.js → RequiresTab-DkuxJsEj.js} +2 -2
  51. package/dist/assets/{RequiresTab-CwaFcz5T.js.map → RequiresTab-DkuxJsEj.js.map} +1 -1
  52. package/dist/assets/{RoutingView-D-qgRSDy.js → RoutingView-BEJ3iJ9c.js} +2 -2
  53. package/dist/assets/{RoutingView-D-qgRSDy.js.map → RoutingView-BEJ3iJ9c.js.map} +1 -1
  54. package/dist/assets/RuntimeTab-DY8ZYeng.css +1 -0
  55. package/dist/assets/RuntimeTab-bWJuLJcn.js +2 -0
  56. package/dist/assets/{RuntimeTab-7GuemvVX.js.map → RuntimeTab-bWJuLJcn.js.map} +1 -1
  57. package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-VAye0ofl.js → SelectMenu.vue_vue_type_script_setup_true_lang-DKYlHroe.js} +2 -2
  58. package/dist/assets/{SelectMenu.vue_vue_type_script_setup_true_lang-VAye0ofl.js.map → SelectMenu.vue_vue_type_script_setup_true_lang-DKYlHroe.js.map} +1 -1
  59. package/dist/assets/{SettingsView-cuiExi3-.js → SettingsView-CgwOmLat.js} +2 -2
  60. package/dist/assets/{SettingsView-cuiExi3-.js.map → SettingsView-CgwOmLat.js.map} +1 -1
  61. package/dist/assets/SkeletonList.vue_vue_type_script_setup_true_lang-Bwex16vy.js +2 -0
  62. package/dist/assets/{SkeletonList.vue_vue_type_script_setup_true_lang-DMdwApyt.js.map → SkeletonList.vue_vue_type_script_setup_true_lang-Bwex16vy.js.map} +1 -1
  63. package/dist/assets/{SpecDetailView-Da_PDlft.js → SpecDetailView-BlHaL1Y8.js} +2 -2
  64. package/dist/assets/{SpecDetailView-Da_PDlft.js.map → SpecDetailView-BlHaL1Y8.js.map} +1 -1
  65. package/dist/assets/{SpecsView-CQHYfu01.js → SpecsView-C-bS2Av2.js} +2 -2
  66. package/dist/assets/{SpecsView-CQHYfu01.js.map → SpecsView-C-bS2Av2.js.map} +1 -1
  67. package/dist/assets/SsoMark.vue_vue_type_script_setup_true_lang-aKprkHCF.js +2 -0
  68. package/dist/assets/SsoMark.vue_vue_type_script_setup_true_lang-aKprkHCF.js.map +1 -0
  69. package/dist/assets/SsoView-BUz2EaVq.js +2 -0
  70. package/dist/assets/SsoView-BUz2EaVq.js.map +1 -0
  71. package/dist/assets/SsoView-JmlPQGWM.css +1 -0
  72. package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-m8_cteEx.js → StateBadge.vue_vue_type_script_setup_true_lang-C-7EQXcg.js} +2 -2
  73. package/dist/assets/{StateBadge.vue_vue_type_script_setup_true_lang-m8_cteEx.js.map → StateBadge.vue_vue_type_script_setup_true_lang-C-7EQXcg.js.map} +1 -1
  74. package/dist/assets/{SubmitView-DsR4Rud8.js → SubmitView-s5haxS9q.js} +2 -2
  75. package/dist/assets/{SubmitView-DsR4Rud8.js.map → SubmitView-s5haxS9q.js.map} +1 -1
  76. package/dist/assets/{SwarmView-B-ESSXnj.js → SwarmView-skNbhHws.js} +2 -2
  77. package/dist/assets/{SwarmView-B-ESSXnj.js.map → SwarmView-skNbhHws.js.map} +1 -1
  78. package/dist/assets/{Teleport-DTF2H_ia.js → Teleport-CKfsIrp8.js} +2 -2
  79. package/dist/assets/{Teleport-DTF2H_ia.js.map → Teleport-CKfsIrp8.js.map} +1 -1
  80. package/dist/assets/{TerminalTab-BboWRxwq.js → TerminalTab-CPuFkMzh.js} +2 -2
  81. package/dist/assets/{TerminalTab-BboWRxwq.js.map → TerminalTab-CPuFkMzh.js.map} +1 -1
  82. package/dist/assets/{UsersView-BBn3F_YM.js → UsersView-CIfFPshj.js} +2 -2
  83. package/dist/assets/{UsersView-BBn3F_YM.js.map → UsersView-CIfFPshj.js.map} +1 -1
  84. package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-D7QFvEED.js → VarEditor.vue_vue_type_script_setup_true_lang-JwR7DqO1.js} +2 -2
  85. package/dist/assets/{VarEditor.vue_vue_type_script_setup_true_lang-D7QFvEED.js.map → VarEditor.vue_vue_type_script_setup_true_lang-JwR7DqO1.js.map} +1 -1
  86. package/dist/assets/{VariablesView-BAajBPNO.js → VariablesView-DatC33mT.js} +2 -2
  87. package/dist/assets/{VariablesView-BAajBPNO.js.map → VariablesView-DatC33mT.js.map} +1 -1
  88. package/dist/assets/{index-BXPaPWhH.js → index-T3JGe9ku.js} +30 -25
  89. package/dist/assets/index-T3JGe9ku.js.map +1 -0
  90. package/dist/assets/useDeployment-CV0kcdoc.js +2 -0
  91. package/dist/assets/{useDeployment-BDHMCIYu.js.map → useDeployment-CV0kcdoc.js.map} +1 -1
  92. package/dist/index.html +1 -1
  93. package/package.json +1 -1
  94. package/dist/assets/LoginView-CSY6YIsK.js +0 -2
  95. package/dist/assets/LoginView-CSY6YIsK.js.map +0 -1
  96. package/dist/assets/LoginView-Djedwklv.css +0 -1
  97. package/dist/assets/RelativeTime.vue_vue_type_script_setup_true_lang-BoNYQAdk.js +0 -2
  98. package/dist/assets/RuntimeTab-7GuemvVX.js +0 -2
  99. package/dist/assets/RuntimeTab-RB6kjod2.css +0 -1
  100. package/dist/assets/SkeletonList.vue_vue_type_script_setup_true_lang-DMdwApyt.js +0 -2
  101. package/dist/assets/SsoView-5Kk12W1K.js +0 -2
  102. package/dist/assets/SsoView-5Kk12W1K.js.map +0 -1
  103. package/dist/assets/index-BXPaPWhH.js.map +0 -1
  104. package/dist/assets/useDeployment-BDHMCIYu.js +0 -2
@@ -0,0 +1,2 @@
1
+ import{W as l,k as i,p as n,C as d,P as p,X as u}from"./index-T3JGe9ku.js";import{w as c,r as y}from"./useVars-VN9gYdR6.js";const e=l({id:"",detail:null,error:"",loading:!1,vars:[]}),f=i(()=>p(e.vars)),k=i(()=>u(e.id)),h=i(()=>{var a;return((a=e.detail)==null?void 0:a.kind)==="shared"}),b=i(()=>{var a;return(((a=e.detail)==null?void 0:a.axes)??[]).filter(s=>!s.verifiable).map(s=>s.name)});async function t(){const a=e.id;e.loading=!0;const s=await n.get(`/api/deployments/${encodeURIComponent(a)}${f.value}`);if(e.id!==a)return;if(e.loading=!1,!s.ok){e.detail=null,e.error=d(s,`GET /api/deployments/${a}`);return}e.error="";const r=s.body;e.detail={id:r.id??a,kind:r.kind==="shared"?"shared":"isolated",createdAt:r.createdAt,updatedAt:r.updatedAt,stack:typeof r.stack=="string"?r.stack:"",busy:r.busy??null,orchestrator:r.orchestrator??null,sleep:r.sleep??null,asleep:r.asleep??null,compose:r.compose?{file:r.compose.file??"?",profiles:r.compose.profiles??[],overlays:r.compose.overlays??[]}:null,requires:Array.isArray(r.requires)?r.requires:[],axes:(Array.isArray(r.axes)?r.axes:[]).map(o=>({name:o.name,hooks:Array.isArray(o.hooks)?o.hooks:[],verifiable:o.verifiable??(Array.isArray(o.hooks)&&o.hooks.includes("assert_gone"))})),env:Array.isArray(r.env)?r.env:[]}}function g(a){e.id===a&&(e.detail||e.error)||(e.id=a,e.detail=null,e.error="",e.vars=y(a),t())}function m(){c(e.id,e.vars)}function x(){m(),t()}export{x as a,e as d,h as i,t as l,g as o,m as p,k as r,b as u,f as v};
2
+ //# sourceMappingURL=useDeployment-CV0kcdoc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useDeployment-BDHMCIYu.js","sources":["../../src/composables/useDeployment.ts"],"sourcesContent":["/**\n * The deployment currently open in the detail view.\n *\n * A module singleton rather than provide/inject, because the six tabs are CHILD ROUTES: they are\n * siblings of each other and are mounted and unmounted independently, so passing the loaded\n * deployment down as a prop would mean re-fetching it on every tab change. Switching tabs must not\n * re-fetch or blank the panel — only opening a DIFFERENT deployment does.\n *\n * `vars` here are REQUEST-TIME variables, sent as `?query` on every route that resolves this\n * deployment. See `useVars.ts` for why they matter and what they can get wrong.\n */\n\nimport { computed, reactive } from 'vue';\nimport { api, problem, query } from '../api/client';\nimport type { Deployment, VarPair } from '../api/types';\nimport { readVars, writeVars } from './useVars';\nimport { rowFor } from './useControlPlane';\n\nexport const dep = reactive({\n id: '',\n detail: null as Deployment | null,\n /** The resolve failure, verbatim. It is a 400 that NAMES the missing variable. */\n error: '',\n loading: false,\n vars: [] as VarPair[],\n});\n\nexport const varsQuery = computed(() => query(dep.vars));\n\n/** The list row, which is the only place the server exposes stored `vars` and `specName` today. */\nexport const row = computed(() => rowFor(dep.id));\n\nexport const isShared = computed(() => dep.detail?.kind === 'shared');\n\nexport const unverifiableAxes = computed(() =>\n (dep.detail?.axes ?? []).filter((a) => !a.verifiable).map((a) => a.name),\n);\n\nexport async function loadDetail(): Promise<void> {\n const id = dep.id;\n dep.loading = true;\n const r = await api.get<Deployment>(`/api/deployments/${encodeURIComponent(id)}${varsQuery.value}`);\n // Navigated to another deployment while this was in flight: drop the answer rather than render\n // one deployment's spec under another's id.\n if (dep.id !== id) return;\n dep.loading = false;\n\n if (!r.ok) {\n dep.detail = null;\n dep.error = problem(r, `GET /api/deployments/${id}`);\n return;\n }\n dep.error = '';\n const b = r.body;\n dep.detail = {\n id: b.id ?? id,\n kind: b.kind === 'shared' ? 'shared' : 'isolated',\n createdAt: b.createdAt,\n updatedAt: b.updatedAt,\n stack: typeof b.stack === 'string' ? b.stack : '',\n busy: b.busy ?? null,\n orchestrator: b.orchestrator ?? null,\n sleep: b.sleep ?? null,\n asleep: b.asleep ?? null,\n compose: b.compose\n ? {\n file: b.compose.file ?? '?',\n profiles: b.compose.profiles ?? [],\n overlays: b.compose.overlays ?? [],\n }\n : null,\n requires: Array.isArray(b.requires) ? b.requires : [],\n axes: (Array.isArray(b.axes) ? b.axes : []).map((a) => ({\n name: a.name,\n hooks: Array.isArray(a.hooks) ? a.hooks : [],\n // Derive it if an older build omits the field. Defaulting to `false` would claim an axis is\n // unverifiable when it may define a perfectly good `assert_gone`.\n verifiable: a.verifiable ?? (Array.isArray(a.hooks) && a.hooks.includes('assert_gone')),\n })),\n env: Array.isArray(b.env) ? b.env : [],\n };\n}\n\n/** Point the singleton at a deployment. Resets everything a stale id could leak into the new one. */\nexport function openDeployment(id: string): void {\n if (dep.id === id && (dep.detail || dep.error)) return; // a tab change, not a navigation\n dep.id = id;\n dep.detail = null;\n dep.error = '';\n dep.vars = readVars(id);\n void loadDetail();\n}\n\nexport function persistVars(): void {\n writeVars(dep.id, dep.vars);\n}\n\nexport function applyVars(): void {\n persistVars();\n void loadDetail();\n}\n"],"names":["dep","reactive","varsQuery","computed","query","row","rowFor","isShared","_a","unverifiableAxes","a","loadDetail","id","r","api","problem","b","openDeployment","readVars","persistVars","writeVars","applyVars"],"mappings":"uHAkBO,MAAMA,EAAMC,EAAS,CAC1B,GAAI,GACJ,OAAQ,KAER,MAAO,GACP,QAAS,GACT,KAAM,CAAA,CACR,CAAC,EAEYC,EAAYC,EAAS,IAAMC,EAAMJ,EAAI,IAAI,CAAC,EAG1CK,EAAMF,EAAS,IAAMG,EAAON,EAAI,EAAE,CAAC,EAEnCO,EAAWJ,EAAS,IAAA,OAAM,QAAAK,EAAAR,EAAI,SAAJ,YAAAQ,EAAY,QAAS,SAAQ,EAEvDC,EAAmBN,EAAS,IAAA,OACtC,SAAAK,EAAAR,EAAI,SAAJ,YAAAQ,EAAY,OAAQ,CAAA,GAAI,OAAQE,GAAM,CAACA,EAAE,UAAU,EAAE,IAAKA,GAAMA,EAAE,IAAI,EACzE,EAEA,eAAsBC,GAA4B,CAChD,MAAMC,EAAKZ,EAAI,GACfA,EAAI,QAAU,GACd,MAAMa,EAAI,MAAMC,EAAI,IAAgB,oBAAoB,mBAAmBF,CAAE,CAAC,GAAGV,EAAU,KAAK,EAAE,EAGlG,GAAIF,EAAI,KAAOY,EAAI,OAGnB,GAFAZ,EAAI,QAAU,GAEV,CAACa,EAAE,GAAI,CACTb,EAAI,OAAS,KACbA,EAAI,MAAQe,EAAQF,EAAG,wBAAwBD,CAAE,EAAE,EACnD,MACF,CACAZ,EAAI,MAAQ,GACZ,MAAMgB,EAAIH,EAAE,KACZb,EAAI,OAAS,CACX,GAAIgB,EAAE,IAAMJ,EACZ,KAAMI,EAAE,OAAS,SAAW,SAAW,WACvC,UAAWA,EAAE,UACb,UAAWA,EAAE,UACb,MAAO,OAAOA,EAAE,OAAU,SAAWA,EAAE,MAAQ,GAC/C,KAAMA,EAAE,MAAQ,KAChB,aAAcA,EAAE,cAAgB,KAChC,MAAOA,EAAE,OAAS,KAClB,OAAQA,EAAE,QAAU,KACpB,QAASA,EAAE,QACP,CACE,KAAMA,EAAE,QAAQ,MAAQ,IACxB,SAAUA,EAAE,QAAQ,UAAY,CAAA,EAChC,SAAUA,EAAE,QAAQ,UAAY,CAAA,CAAC,EAEnC,KACJ,SAAU,MAAM,QAAQA,EAAE,QAAQ,EAAIA,EAAE,SAAW,CAAA,EACnD,MAAO,MAAM,QAAQA,EAAE,IAAI,EAAIA,EAAE,KAAO,CAAA,GAAI,IAAKN,IAAO,CACtD,KAAMA,EAAE,KACR,MAAO,MAAM,QAAQA,EAAE,KAAK,EAAIA,EAAE,MAAQ,CAAA,EAG1C,WAAYA,EAAE,aAAe,MAAM,QAAQA,EAAE,KAAK,GAAKA,EAAE,MAAM,SAAS,aAAa,EAAA,EACrF,EACF,IAAK,MAAM,QAAQM,EAAE,GAAG,EAAIA,EAAE,IAAM,CAAA,CAAC,CAEzC,CAGO,SAASC,EAAeL,EAAkB,CAC3CZ,EAAI,KAAOY,IAAOZ,EAAI,QAAUA,EAAI,SACxCA,EAAI,GAAKY,EACTZ,EAAI,OAAS,KACbA,EAAI,MAAQ,GACZA,EAAI,KAAOkB,EAASN,CAAE,EACjBD,EAAA,EACP,CAEO,SAASQ,GAAoB,CAClCC,EAAUpB,EAAI,GAAIA,EAAI,IAAI,CAC5B,CAEO,SAASqB,GAAkB,CAChCF,EAAA,EACKR,EAAA,CACP"}
1
+ {"version":3,"file":"useDeployment-CV0kcdoc.js","sources":["../../src/composables/useDeployment.ts"],"sourcesContent":["/**\n * The deployment currently open in the detail view.\n *\n * A module singleton rather than provide/inject, because the six tabs are CHILD ROUTES: they are\n * siblings of each other and are mounted and unmounted independently, so passing the loaded\n * deployment down as a prop would mean re-fetching it on every tab change. Switching tabs must not\n * re-fetch or blank the panel — only opening a DIFFERENT deployment does.\n *\n * `vars` here are REQUEST-TIME variables, sent as `?query` on every route that resolves this\n * deployment. See `useVars.ts` for why they matter and what they can get wrong.\n */\n\nimport { computed, reactive } from 'vue';\nimport { api, problem, query } from '../api/client';\nimport type { Deployment, VarPair } from '../api/types';\nimport { readVars, writeVars } from './useVars';\nimport { rowFor } from './useControlPlane';\n\nexport const dep = reactive({\n id: '',\n detail: null as Deployment | null,\n /** The resolve failure, verbatim. It is a 400 that NAMES the missing variable. */\n error: '',\n loading: false,\n vars: [] as VarPair[],\n});\n\nexport const varsQuery = computed(() => query(dep.vars));\n\n/** The list row, which is the only place the server exposes stored `vars` and `specName` today. */\nexport const row = computed(() => rowFor(dep.id));\n\nexport const isShared = computed(() => dep.detail?.kind === 'shared');\n\nexport const unverifiableAxes = computed(() =>\n (dep.detail?.axes ?? []).filter((a) => !a.verifiable).map((a) => a.name),\n);\n\nexport async function loadDetail(): Promise<void> {\n const id = dep.id;\n dep.loading = true;\n const r = await api.get<Deployment>(`/api/deployments/${encodeURIComponent(id)}${varsQuery.value}`);\n // Navigated to another deployment while this was in flight: drop the answer rather than render\n // one deployment's spec under another's id.\n if (dep.id !== id) return;\n dep.loading = false;\n\n if (!r.ok) {\n dep.detail = null;\n dep.error = problem(r, `GET /api/deployments/${id}`);\n return;\n }\n dep.error = '';\n const b = r.body;\n dep.detail = {\n id: b.id ?? id,\n kind: b.kind === 'shared' ? 'shared' : 'isolated',\n createdAt: b.createdAt,\n updatedAt: b.updatedAt,\n stack: typeof b.stack === 'string' ? b.stack : '',\n busy: b.busy ?? null,\n orchestrator: b.orchestrator ?? null,\n sleep: b.sleep ?? null,\n asleep: b.asleep ?? null,\n compose: b.compose\n ? {\n file: b.compose.file ?? '?',\n profiles: b.compose.profiles ?? [],\n overlays: b.compose.overlays ?? [],\n }\n : null,\n requires: Array.isArray(b.requires) ? b.requires : [],\n axes: (Array.isArray(b.axes) ? b.axes : []).map((a) => ({\n name: a.name,\n hooks: Array.isArray(a.hooks) ? a.hooks : [],\n // Derive it if an older build omits the field. Defaulting to `false` would claim an axis is\n // unverifiable when it may define a perfectly good `assert_gone`.\n verifiable: a.verifiable ?? (Array.isArray(a.hooks) && a.hooks.includes('assert_gone')),\n })),\n env: Array.isArray(b.env) ? b.env : [],\n };\n}\n\n/** Point the singleton at a deployment. Resets everything a stale id could leak into the new one. */\nexport function openDeployment(id: string): void {\n if (dep.id === id && (dep.detail || dep.error)) return; // a tab change, not a navigation\n dep.id = id;\n dep.detail = null;\n dep.error = '';\n dep.vars = readVars(id);\n void loadDetail();\n}\n\nexport function persistVars(): void {\n writeVars(dep.id, dep.vars);\n}\n\nexport function applyVars(): void {\n persistVars();\n void loadDetail();\n}\n"],"names":["dep","reactive","varsQuery","computed","query","row","rowFor","isShared","_a","unverifiableAxes","a","loadDetail","id","r","api","problem","b","openDeployment","readVars","persistVars","writeVars","applyVars"],"mappings":"4HAkBO,MAAMA,EAAMC,EAAS,CAC1B,GAAI,GACJ,OAAQ,KAER,MAAO,GACP,QAAS,GACT,KAAM,CAAA,CACR,CAAC,EAEYC,EAAYC,EAAS,IAAMC,EAAMJ,EAAI,IAAI,CAAC,EAG1CK,EAAMF,EAAS,IAAMG,EAAON,EAAI,EAAE,CAAC,EAEnCO,EAAWJ,EAAS,IAAA,OAAM,QAAAK,EAAAR,EAAI,SAAJ,YAAAQ,EAAY,QAAS,SAAQ,EAEvDC,EAAmBN,EAAS,IAAA,OACtC,SAAAK,EAAAR,EAAI,SAAJ,YAAAQ,EAAY,OAAQ,CAAA,GAAI,OAAQE,GAAM,CAACA,EAAE,UAAU,EAAE,IAAKA,GAAMA,EAAE,IAAI,EACzE,EAEA,eAAsBC,GAA4B,CAChD,MAAMC,EAAKZ,EAAI,GACfA,EAAI,QAAU,GACd,MAAMa,EAAI,MAAMC,EAAI,IAAgB,oBAAoB,mBAAmBF,CAAE,CAAC,GAAGV,EAAU,KAAK,EAAE,EAGlG,GAAIF,EAAI,KAAOY,EAAI,OAGnB,GAFAZ,EAAI,QAAU,GAEV,CAACa,EAAE,GAAI,CACTb,EAAI,OAAS,KACbA,EAAI,MAAQe,EAAQF,EAAG,wBAAwBD,CAAE,EAAE,EACnD,MACF,CACAZ,EAAI,MAAQ,GACZ,MAAMgB,EAAIH,EAAE,KACZb,EAAI,OAAS,CACX,GAAIgB,EAAE,IAAMJ,EACZ,KAAMI,EAAE,OAAS,SAAW,SAAW,WACvC,UAAWA,EAAE,UACb,UAAWA,EAAE,UACb,MAAO,OAAOA,EAAE,OAAU,SAAWA,EAAE,MAAQ,GAC/C,KAAMA,EAAE,MAAQ,KAChB,aAAcA,EAAE,cAAgB,KAChC,MAAOA,EAAE,OAAS,KAClB,OAAQA,EAAE,QAAU,KACpB,QAASA,EAAE,QACP,CACE,KAAMA,EAAE,QAAQ,MAAQ,IACxB,SAAUA,EAAE,QAAQ,UAAY,CAAA,EAChC,SAAUA,EAAE,QAAQ,UAAY,CAAA,CAAC,EAEnC,KACJ,SAAU,MAAM,QAAQA,EAAE,QAAQ,EAAIA,EAAE,SAAW,CAAA,EACnD,MAAO,MAAM,QAAQA,EAAE,IAAI,EAAIA,EAAE,KAAO,CAAA,GAAI,IAAKN,IAAO,CACtD,KAAMA,EAAE,KACR,MAAO,MAAM,QAAQA,EAAE,KAAK,EAAIA,EAAE,MAAQ,CAAA,EAG1C,WAAYA,EAAE,aAAe,MAAM,QAAQA,EAAE,KAAK,GAAKA,EAAE,MAAM,SAAS,aAAa,EAAA,EACrF,EACF,IAAK,MAAM,QAAQM,EAAE,GAAG,EAAIA,EAAE,IAAM,CAAA,CAAC,CAEzC,CAGO,SAASC,EAAeL,EAAkB,CAC3CZ,EAAI,KAAOY,IAAOZ,EAAI,QAAUA,EAAI,SACxCA,EAAI,GAAKY,EACTZ,EAAI,OAAS,KACbA,EAAI,MAAQ,GACZA,EAAI,KAAOkB,EAASN,CAAE,EACjBD,EAAA,EACP,CAEO,SAASQ,GAAoB,CAClCC,EAAUpB,EAAI,GAAIA,EAAI,IAAI,CAC5B,CAEO,SAASqB,GAAkB,CAChCF,EAAA,EACKR,EAAA,CACP"}
package/dist/index.html CHANGED
@@ -27,7 +27,7 @@
27
27
  /* private mode: fall through to the OS preference */
28
28
  }
29
29
  </script>
30
- <script type="module" crossorigin src="/assets/index-BXPaPWhH.js"></script>
30
+ <script type="module" crossorigin src="/assets/index-T3JGe9ku.js"></script>
31
31
  <link rel="stylesheet" crossorigin href="/assets/index-B_k9b7xG.css">
32
32
  </head>
33
33
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@samyx/preview-stacks-ui",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "private": false,
5
5
  "description": "The advanced web UI for preview-stacks — an opt-in SPA that talks to the pstack HTTP API.",
6
6
  "type": "module",
@@ -1,2 +0,0 @@
1
- import{d as E,c as i,a as t,u as v,b as f,F as _,e as o,f as s,w as l,_ as S,g as c,t as u,h as I,i as C,v as q,r as g,j as V,k as L,l as B,m as P,o as d,n as $,p as z,q as A}from"./index-BXPaPWhH.js";import{_ as U}from"./ActionButton.vue_vue_type_script_setup_true_lang-CxT4O7XN.js";import{E as D}from"./EquivalentCommand-BjlPIrt9.js";import{_ as F}from"./_plugin-vue_export-helper-DlAUqK2U.js";const M={class:"login-wrap"},W={class:"login-card panel"},j={class:"banner plain",style:{"margin-top":"var(--s3)"}},O={class:"mute",style:{"margin-top":"var(--s3)"}},G={key:0,class:"banner failed",style:{"margin-top":"var(--s3)"}},H={class:"field",style:{"margin-top":"var(--s4)"}},J={class:"field"},K={key:0,class:"banner failed",style:{"margin-top":"var(--s3)"}},Q={class:"row",style:{"margin-top":"var(--s4)"}},X=E({__name:"LoginView",setup(Y){const N=B(),a=P(),p=g(""),n=g(""),r=g(!1),m=g(""),w=V(()=>typeof a.query.next=="string"&&a.query.next.startsWith("/")?a.query.next:"/"),b=V(()=>typeof a.query.sso_error=="string"?a.query.sso_error:"");function R(){window.location.assign($.url(`/api/auth/sso/start?next=${encodeURIComponent(w.value)}`))}async function k(){r.value=!0,m.value="";const y=await z(p.value.trim(),n.value);if(r.value=!1,y){m.value=y,n.value="";return}await A(),N.replace(w.value)}return(y,e)=>{var x;const T=L("RouterLink");return d(),i("div",M,[t("div",W,[e[13]||(e[13]=t("h1",{style:{"font-size":"var(--t-xl)","margin-bottom":"var(--s2)"}},"Sign in",-1)),v(f).hasUsers===!1?(d(),i(_,{key:0},[e[6]||(e[6]=t("p",{class:"dim"}," No accounts exist on this server yet — create the first one, then sign in. ",-1)),t("div",j,[e[4]||(e[4]=t("b",null,"Create the first account",-1)),e[5]||(e[5]=t("p",null," This has to be done with the host token, from a terminal — a browser has no way to prove it is allowed to claim a fresh server. ",-1)),o(D,{what:"creating the first account",method:"POST",path:"/api/auth/bootstrap",body:{username:"you",password:"…"}}),t("p",O,[e[3]||(e[3]=s(" It can also be set up ahead of time when the host is first provisioned. ",-1)),o(S,{label:"how"},{default:l(()=>[...e[2]||(e[2]=[s(" Set the admin username and password in the environment before running the setup command on the host. They are honoured only while no account exists, so they cannot overwrite one later. ",-1)])]),_:1})])])],64)):c("",!0),(x=v(f).sso)!=null&&x.enabled?(d(),i(_,{key:1},[b.value?(d(),i("div",G,[t("b",null,"Signing in with "+u(v(f).sso.label)+" did not work.",1),t("p",null,u(b.value),1)])):c("",!0),o(U,{variant:"primary",style:{width:"100%","margin-top":"var(--s4)"},onClick:R},{default:l(()=>[s(" Sign in with "+u(v(f).sso.label),1)]),_:1}),e[7]||(e[7]=t("div",{class:"row",style:{margin:"var(--s4) 0","align-items":"center",gap:"var(--s3)"}},[t("hr",{style:{flex:"1",border:"0","border-top":"1px solid var(--line)"}}),t("span",{class:"mute",style:{"font-size":"var(--t-sm)"}},"or with a local account"),t("hr",{style:{flex:"1",border:"0","border-top":"1px solid var(--line)"}})],-1))],64)):c("",!0),t("form",{onSubmit:I(k,["prevent"])},[t("div",H,[e[8]||(e[8]=t("label",{for:"u"},"Username",-1)),C(t("input",{id:"u","onUpdate:modelValue":e[0]||(e[0]=h=>p.value=h),type:"text",autocomplete:"username",spellcheck:"false"},null,512),[[q,p.value]])]),t("div",J,[e[9]||(e[9]=t("label",{for:"p"},"Password",-1)),C(t("input",{id:"p","onUpdate:modelValue":e[1]||(e[1]=h=>n.value=h),type:"password",autocomplete:"current-password"},null,512),[[q,n.value]])]),m.value?(d(),i("div",K,[t("p",null,u(m.value),1)])):c("",!0),t("div",Q,[o(U,{variant:"primary",pending:r.value,disabled:r.value||!p.value.trim()||!n.value,onClick:k},{default:l(()=>[s(u(r.value?"Signing in…":"Sign in"),1)]),_:1},8,["pending","disabled"]),e[12]||(e[12]=t("span",{class:"grow"},null,-1)),o(T,{to:"/settings",class:"mute"},{default:l(()=>[e[11]||(e[11]=s(" Use a token instead ",-1)),o(S,{label:"token access",align:"end"},{default:l(()=>[...e[10]||(e[10]=[s(" A bearer token in Settings — the machine token from ",-1),t("code",null,"pstack init",-1),s(", or a personal token from your account — authenticates every request without a session. ",-1)])]),_:1})]),_:1})])],32)])])}}}),oe=F(X,[["__scopeId","data-v-06638fa7"]]);export{oe as default};
2
- //# sourceMappingURL=LoginView-CSY6YIsK.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LoginView-CSY6YIsK.js","sources":["../../src/views/LoginView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Sign in — the one page reachable without a session.\n *\n * Two states it must distinguish, because they need opposite instructions:\n * - accounts exist → the form\n * - no accounts yet → bootstrap guidance. A dead login form with no way to create the\n * account it asks for is the worst version of this page.\n *\n * A bearer token in Settings also authenticates (CI-style access); the link stays visible for\n * operators who have the machine token and no account yet.\n */\nimport { computed, ref } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport { authState, checkAuth, login } from '../composables/useAuth';\nimport { api } from '../api/client';\nimport ActionButton from '../components/ActionButton.vue';\nimport InfoHint from '../components/InfoHint.vue';\nimport EquivalentCommand from '../components/EquivalentCommand.vue';\n\nconst router = useRouter();\nconst route = useRoute();\n\nconst username = ref('');\nconst password = ref('');\nconst pending = ref(false);\nconst error = ref('');\n\n/** Where the guard wanted to go. Also what the SSO round trip carries and comes back to. */\nconst next = computed(() =>\n typeof route.query.next === 'string' && route.query.next.startsWith('/') ? route.query.next : '/',\n);\n\n/**\n * The provider failed or refused. It arrives in the query string because the callback is a REDIRECT\n * — there is no fetch to read a body from — and it is the provider's own words, so it renders as\n * text and never as markup.\n */\nconst ssoError = computed(() => (typeof route.query.sso_error === 'string' ? route.query.sso_error : ''));\n\n/**\n * A full navigation, not a fetch: the whole point is to leave this origin for the provider's\n * consent screen. `api.url` so a dev session pointed at a tunnelled host still reaches its API.\n */\nfunction signInWithProvider(): void {\n window.location.assign(api.url(`/api/auth/sso/start?next=${encodeURIComponent(next.value)}`));\n}\n\nasync function submit(): Promise<void> {\n pending.value = true;\n error.value = '';\n const failure = await login(username.value.trim(), password.value);\n pending.value = false;\n if (failure) {\n error.value = failure;\n password.value = '';\n return;\n }\n await checkAuth();\n // Back to wherever the guard bounced them from, or home.\n void router.replace(next.value);\n}\n</script>\n\n<template>\n <div class=\"login-wrap\">\n <div class=\"login-card panel\">\n <h1 style=\"font-size: var(--t-xl); margin-bottom: var(--s2)\">Sign in</h1>\n\n <template v-if=\"authState.hasUsers === false\">\n <p class=\"dim\">\n No accounts exist on this server yet — create the first one, then sign in.\n </p>\n <!--\n A curl block used to sit here permanently, on the FIRST screen anyone sees. It is needed\n exactly once per host and by one person; behind the button it costs a click then and\n nothing on every later visit — and it is generated, so it cannot drift from the route.\n -->\n <div class=\"banner plain\" style=\"margin-top: var(--s3)\">\n <b>Create the first account</b>\n <p>\n This has to be done with the host token, from a terminal — a browser has no way to prove\n it is allowed to claim a fresh server.\n </p>\n <EquivalentCommand\n what=\"creating the first account\"\n method=\"POST\"\n path=\"/api/auth/bootstrap\"\n :body=\"{ username: 'you', password: '…' }\"\n />\n <p class=\"mute\" style=\"margin-top: var(--s3)\">\n It can also be set up ahead of time when the host is first provisioned.\n <InfoHint label=\"how\">\n Set the admin username and password in the environment before running the setup\n command on the host. They are honoured only while no account exists, so they cannot\n overwrite one later.\n </InfoHint>\n </p>\n </div>\n </template>\n\n <!-- ── the provider, above the form: for most people on a host with SSO it is the only\n control on this page they should touch. -->\n <template v-if=\"authState.sso?.enabled\">\n <div v-if=\"ssoError\" class=\"banner failed\" style=\"margin-top: var(--s3)\">\n <b>Signing in with {{ authState.sso.label }} did not work.</b>\n <p>{{ ssoError }}</p>\n </div>\n <ActionButton variant=\"primary\" style=\"width: 100%; margin-top: var(--s4)\" @click=\"signInWithProvider\">\n Sign in with {{ authState.sso.label }}\n </ActionButton>\n <div class=\"row\" style=\"margin: var(--s4) 0; align-items: center; gap: var(--s3)\">\n <hr style=\"flex: 1; border: 0; border-top: 1px solid var(--line)\" />\n <span class=\"mute\" style=\"font-size: var(--t-sm)\">or with a local account</span>\n <hr style=\"flex: 1; border: 0; border-top: 1px solid var(--line)\" />\n </div>\n </template>\n\n <form @submit.prevent=\"submit\">\n <div class=\"field\" style=\"margin-top: var(--s4)\">\n <label for=\"u\">Username</label>\n <input id=\"u\" v-model=\"username\" type=\"text\" autocomplete=\"username\" spellcheck=\"false\" />\n </div>\n <div class=\"field\">\n <label for=\"p\">Password</label>\n <input id=\"p\" v-model=\"password\" type=\"password\" autocomplete=\"current-password\" />\n </div>\n\n <div v-if=\"error\" class=\"banner failed\" style=\"margin-top: var(--s3)\">\n <p>{{ error }}</p>\n </div>\n\n <div class=\"row\" style=\"margin-top: var(--s4)\">\n <ActionButton\n variant=\"primary\"\n :pending=\"pending\"\n :disabled=\"pending || !username.trim() || !password\"\n @click=\"submit\"\n >\n {{ pending ? 'Signing in…' : 'Sign in' }}\n </ActionButton>\n <span class=\"grow\" />\n <RouterLink to=\"/settings\" class=\"mute\">\n Use a token instead\n <InfoHint label=\"token access\" align=\"end\">\n A bearer token in Settings — the machine token from <code>pstack init</code>, or a\n personal token from your account — authenticates every request without a session.\n </InfoHint>\n </RouterLink>\n </div>\n </form>\n </div>\n </div>\n</template>\n\n<style scoped>\n/* The shell centres this now — see `.shell.no-rail .view`. A second centring context here would\n * fight it, and `min-height: 70vh` was what pinned the card toward the top of the window. */\n/* `100%`, not `92vw`: the shell already insets this by its own padding, so a viewport-relative\n * width added on top overflowed the right edge on a phone (measured 32px/0px at 390). */\n.login-wrap {\n width: min(420px, 100%);\n}\n.login-card {\n width: 100%;\n}\n</style>\n"],"names":["router","useRouter","route","useRoute","username","ref","password","pending","error","next","computed","ssoError","signInWithProvider","api","submit","failure","login","checkAuth","_openBlock","_createElementBlock","_hoisted_1","_createElementVNode","_hoisted_2","_cache","_unref","authState","_Fragment","_hoisted_3","_createVNode","EquivalentCommand","_hoisted_4","InfoHint","_hoisted_5","_toDisplayString","ActionButton","_createTextVNode","_hoisted_6","$event","_hoisted_7","_hoisted_8","_hoisted_9","_component_RouterLink"],"mappings":"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"}
@@ -1 +0,0 @@
1
- .login-wrap[data-v-06638fa7]{width:min(420px,100%)}.login-card[data-v-06638fa7]{width:100%}
@@ -1,2 +0,0 @@
1
- import{c as i,b as l}from"./useFormat-DIlTY5Dl.js";import{d as r,K as c,a6 as m,o as u,c as d,u as f,t as p,r as v,j as w}from"./index-BXPaPWhH.js";const D=["datetime","title"],I=15e3,h=r({__name:"RelativeTime",props:{at:{}},setup(e){const o=e,n=v(Date.now());let t=null,a=0;c(()=>{a++,t||(t=setInterval(()=>n.value=Date.now(),I))}),m(()=>{a--,a===0&&t&&(clearInterval(t),t=null)});const s=w(()=>(n.value,l(o.at)));return(S,_)=>(u(),d("time",{datetime:e.at?new Date(e.at).toISOString():void 0,title:f(i)(e.at)},p(s.value),9,D))}});export{h as _};
2
- //# sourceMappingURL=RelativeTime.vue_vue_type_script_setup_true_lang-BoNYQAdk.js.map
@@ -1,2 +0,0 @@
1
- import{s as x}from"./useFormat-DIlTY5Dl.js";import{d as W,J as H,s as M,c as o,z as k,g as u,F as p,a as s,y as f,f as l,t as r,e as y,w as g,_ as U,r as R,j as c,k as j,o as n,D as z,E as T,u as S,n as L,C as A,O as D}from"./index-BXPaPWhH.js";import{d as b,v as B}from"./useDeployment-BDHMCIYu.js";import{_ as J}from"./SkeletonList.vue_vue_type_script_setup_true_lang-DMdwApyt.js";import{_ as I}from"./ActionButton.vue_vue_type_script_setup_true_lang-CxT4O7XN.js";import{_ as Q}from"./ErrorNote.vue_vue_type_script_setup_true_lang-B5-EJpIO.js";import{_ as q}from"./RefreshButton.vue_vue_type_script_setup_true_lang-C7qymrfD.js";import{_ as G}from"./_plugin-vue_export-helper-DlAUqK2U.js";import"./useVars-VN9gYdR6.js";const X={key:0,class:"banner warn"},Y={key:0,class:"banner ok"},Z={class:"panel"},ee={class:"phead"},te={class:"mute",style:{"font-size":"var(--t-sm)"}},se={key:0,class:"cards"},ne={class:"stagger"},oe={"data-label":"url"},ae=["href"],re={key:1,class:"mono mute"},le={key:0,class:"mute"},ie={"data-label":"forwards to"},de={key:0,class:"mono"},ue={key:1,class:"s-failed"},me={class:"mute",style:{"font-size":"var(--t-sm)"}},pe={"data-label":"router"},he={key:0,class:"mute",style:{"font-size":"var(--t-sm)"}},ve={"data-label":"tls"},fe={key:0,class:"badge ok"},ge={key:1,class:"badge off"},ke={key:1,class:"banner warn"},ye={class:"panel"},ce={class:"phead"},be={class:"mute",style:{"font-size":"var(--t-sm)"}},_e={key:0,class:"cards"},we={key:0},$e={class:"stagger"},Re={class:"name","data-label":"service"},Se=["title"],Ce={key:0,"data-label":"node"},Te={key:0,class:"badge",title:"runs on another swarm node — logs reach it through the manager; a shell and stop/start do not"},Ie={"data-label":"state"},Ne={"data-label":"ports"},Pe={class:"mono"},xe={key:0,class:"mute"},Ue={key:0,class:"mute"},ze={"data-label":"networks"},Le={key:0,class:"mute mono",style:{"font-size":"var(--t-sm)"}},Ae={class:"dim","data-label":"image"},De={class:"row-actions","data-label":""},Be={key:0,class:"mute",style:{"font-size":"var(--t-sm)"},title:"runs on another swarm node — redeploy the stack, or act on the worker itself"},Ee=["href"],Ve={key:1,class:"mute"},Fe=W({__name:"RuntimeTab",setup(Ke){const i=R(null),_=R(""),w=R(!0);async function $(){const a=await L.get(`/api/deployments/${encodeURIComponent(b.id)}/runtime${B.value}`);if(w.value=!1,!a.ok){_.value=A(a,"inspect what is running");return}_.value="",i.value=a.body}H(()=>b.id,()=>{w.value=!0,$()},{immediate:!0}),M(()=>void $(),8e3);const E=c(()=>{var a;return((a=i.value)==null?void 0:a.findings.filter(e=>e.level==="error"))??[]}),V=c(()=>{var a;return((a=i.value)==null?void 0:a.findings.filter(e=>e.level==="warn"))??[]}),F=c(()=>{var a;return((a=i.value)==null?void 0:a.findings.filter(e=>e.level==="info"))??[]}),K=c(()=>{var a;return((a=i.value)==null?void 0:a.containers.filter(e=>e.state==="running").length)??0}),N=c(()=>{var a;return((a=i.value)==null?void 0:a.containers.some(e=>e.node))??!1});function O(a){const e=a.split(".");return e.length===3?`${e[1]}.${e[2]}`:a}const v=R("");async function C(a,e){v.value=`${e}:${a.name}`;const m=await L.post(`/api/deployments/${encodeURIComponent(b.id)}/containers/${encodeURIComponent(a.name)}/${e}${B.value}`);if(v.value="",!m.ok){D("error",A(m,`${e} ${a.name}`));return}D("ok",m.body.note??`${a.name}: ${e} done.`),$()}function P(a){var m;const e=(m=i.value)==null?void 0:m.routes.filter(t=>t.container===a.name||t.container===a.service).flatMap(t=>t.hosts).find(t=>!t.startsWith("(pattern)"));return e?`https://${e}`:null}return(a,e)=>{const m=j("RouterLink");return n(),o("div",null,[_.value?(n(),k(Q,{key:0,text:_.value,title:"Could not inspect this deployment."},null,8,["text"])):u("",!0),w.value&&!i.value?(n(),k(J,{key:1,rows:5})):i.value?(n(),o(p,{key:2},[i.value.reachable?(n(),o(p,{key:1},[(n(!0),o(p,null,f(E.value,(t,h)=>(n(),o("div",{key:`e${h}`,class:"banner failed"},[e[1]||(e[1]=s("b",null,"Blocking",-1)),s("p",null,r(t.message),1)]))),128)),(n(!0),o(p,null,f(V.value,(t,h)=>(n(),o("div",{key:`w${h}`,class:"banner warn"},[e[2]||(e[2]=s("b",null,"Worth checking",-1)),s("p",null,r(t.message),1)]))),128)),(n(!0),o(p,null,f(F.value,(t,h)=>(n(),o("div",{key:`i${h}`,class:"banner plain"},[s("p",null,r(t.message),1)]))),128)),!i.value.findings.length&&i.value.containers.length?(n(),o("div",Y,[...e[3]||(e[3]=[s("b",null,"Nothing looks wrong.",-1),s("p",null," Every container is attached to the ingress network and every router has a rule and a port. If a hostname still fails, the next suspects are DNS and the certificate. ",-1)])])):u("",!0),s("section",Z,[s("div",ee,[e[5]||(e[5]=s("h2",{class:"section"},"Routing",-1)),e[6]||(e[6]=s("span",{class:"grow"},null,-1)),s("span",te,[l(" TLS: "+r(i.value.challenge==="unknown"?"unknown":i.value.challenge)+" ",1),y(U,{label:"why the TLS mode matters here",align:"end"},{default:g(()=>[...e[4]||(e[4]=[l(" Under HTTP-01 every per-PR router needs ",-1),s("code",null,"tls.certresolver=le",-1),l(", because each hostname resolves its own certificate. Under DNS-01 that inverts — one always-on router holds the wildcard, and a per-PR certresolver makes each PR order its own and burn the weekly limit. Read from the running Traefik's own flags, not configured here. ",-1)])]),_:1})])]),i.value.routes.length?(n(),o("table",se,[s("thead",null,[s("tr",null,[e[9]||(e[9]=s("th",null,"URL",-1)),s("th",null,[e[8]||(e[8]=l(" Forwards to ",-1)),y(U,{label:"how a URL reaches a port"},{default:g(()=>[...e[7]||(e[7]=[l(" Traefik's docker provider resolves the container's IP on ",-1),s("code",null,"preview-ingress",-1),l(" and dials it on the port from ",-1),s("code",null,"loadbalancer.server.port",-1),l(" — it does ",-1),s("em",null,"not",-1),l(" use ",-1),s("code",null,"service:port",-1),l(" DNS. So the port here is the one inside the container, and publishing a host port is unnecessary. ",-1)])]),_:1})]),e[10]||(e[10]=s("th",null,"Router",-1)),e[11]||(e[11]=s("th",null,"TLS",-1))])]),s("tbody",ne,[(n(!0),o(p,null,f(i.value.routes,(t,h)=>(n(),o("tr",{key:t.router,style:z({"--i":h})},[s("td",oe,[(n(!0),o(p,null,f(t.hosts,d=>(n(),o("div",{key:d},[d.startsWith("(pattern)")?(n(),o("span",re,r(d),1)):(n(),o("a",{key:0,href:`https://${d}`,target:"_blank",rel:"noreferrer"},r(d),9,ae))]))),128)),t.hosts.length?u("",!0):(n(),o("span",le,"no host in the rule"))]),s("td",ie,[t.target?(n(),o("span",de,r(t.target),1)):(n(),o("span",ue,r(t.port===null?"no port declared":"not on the ingress network"),1)),s("div",me,r(t.container),1)]),s("td",pe,[l(r(t.router)+" ",1),t.priority?(n(),o("div",he," priority "+r(t.priority),1)):u("",!0)]),s("td",ve,[t.tls?(n(),o("span",fe,r(t.certresolver||"inherited"),1)):(n(),o("span",ge,"off"))])],4))),128))])])):(n(),o("div",ke,[e[17]||(e[17]=s("b",null,"No routes. Traefik has not been told about this deployment.",-1)),s("p",null,[e[13]||(e[13]=l(" Routes come from labels in ",-1)),e[14]||(e[14]=s("em",null,"your",-1)),e[15]||(e[15]=l(" compose file, not from pstack and not from the ",-1)),y(m,{to:"/routing"},{default:g(()=>[...e[12]||(e[12]=[l("Routing",-1)])]),_:1}),e[16]||(e[16]=l(" page (that is Traefik's file provider — middleware and TLS options, not per-PR routers). A reachable service needs four things: ",-1))]),e[18]||(e[18]=s("pre",{class:"code"},"labels:\n - traefik.enable=true # the host runs exposedbydefault=false\n - traefik.docker.network=preview-ingress # which network Traefik should dial\n - traefik.http.routers.<name>-${STACK}.rule=Host(`<name>-${STACK}.${PREVIEW_DOMAIN}`)\n - traefik.http.services.<name>-${STACK}.loadbalancer.server.port=80\nnetworks: [default, preview-ingress] # and preview-ingress: { external: true }",-1)),e[19]||(e[19]=s("p",{class:"mute"},[l(" Put "),s("code",null,"${STACK}"),l(" in the router and service names: Traefik's namespace is global across the host, so two deployments sharing a router name serve each other's containers. ")],-1))]))]),s("section",ye,[s("div",ce,[e[20]||(e[20]=s("h2",{class:"section"},"Containers",-1)),e[21]||(e[21]=s("span",{class:"grow"},null,-1)),s("span",be,r(K.value)+" of "+r(i.value.containers.length)+" running ",1),y(q,{run:$,busy:w.value},null,8,["busy"])]),i.value.containers.length?(n(),o("table",_e,[s("thead",null,[s("tr",null,[e[22]||(e[22]=s("th",null,"Service",-1)),N.value?(n(),o("th",we,"Node")):u("",!0),e[23]||(e[23]=s("th",null,"State",-1)),e[24]||(e[24]=s("th",null,"Ports",-1)),e[25]||(e[25]=s("th",null,"Networks",-1)),e[26]||(e[26]=s("th",null,"Image",-1)),e[27]||(e[27]=s("th",{"aria-label":"Actions"},null,-1))])]),s("tbody",$e,[(n(!0),o(p,null,f(i.value.containers,(t,h)=>(n(),o("tr",{key:t.id,style:z({"--i":h})},[s("td",Re,[l(r(t.service??t.name)+" ",1),s("div",{class:"mute task",title:t.name},r(O(t.name)),9,Se)]),N.value?(n(),o("td",Ce,[l(r(t.node??"—")+" ",1),t.remote?(n(),o("span",Te,"remote")):u("",!0)])):u("",!0),s("td",Ie,[s("span",{class:T(t.state==="running"?"s-ok":"s-failed")},r(S(x)(t.state)),3),t.health&&t.state==="running"?(n(),o("div",{key:0,class:T(["mute",t.health==="healthy"?"s-ok":"s-failed"]),style:{"font-size":"var(--t-sm)"}},r(S(x)(t.health)),3)):u("",!0)]),s("td",Ne,[(n(!0),o(p,null,f(t.ports,d=>(n(),o("div",{key:`${d.containerPort}/${d.protocol}`},[s("span",Pe,r(d.containerPort),1),d.hostPort?(n(),o("span",xe," ← host "+r(d.hostPort),1)):u("",!0)]))),128)),t.ports.length?u("",!0):(n(),o("span",Ue,"none exposed"))]),s("td",ze,[(n(!0),o(p,null,f(t.networks,d=>(n(),o("div",{key:d},[s("span",{class:T(d==="preview-ingress"?"s-ok":"")},r(d),3)]))),128)),t.ingressIp?(n(),o("div",Le,r(t.ingressIp),1)):u("",!0)]),s("td",Ae,r(t.image),1),s("td",De,[t.remote?(n(),o("span",Be," on "+r(t.node),1)):t.state!=="running"?(n(),k(I,{key:1,class:"sm",variant:"ghost",pending:v.value===`start:${t.name}`,disabled:!!v.value,confirm:"Start it?",onRun:d=>C(t,"start")},{default:g(()=>[...e[28]||(e[28]=[l(" Start ",-1)])]),_:1},8,["pending","disabled","onRun"])):(n(),k(I,{key:2,class:"sm",variant:"ghost",pending:v.value===`stop:${t.name}`,disabled:!!v.value,confirm:"Stop it?",title:"Stops this container only. It stays stopped until something starts it.",onRun:d=>C(t,"stop")},{default:g(()=>[...e[29]||(e[29]=[l(" Stop ",-1)])]),_:1},8,["pending","disabled","onRun"])),t.remote?u("",!0):(n(),k(I,{key:3,class:"sm",variant:"ghost",pending:v.value===`restart:${t.name}`,disabled:!!v.value,confirm:"Restart it?",onRun:d=>C(t,"restart")},{default:g(()=>[...e[30]||(e[30]=[l(" Restart ",-1)])]),_:1},8,["pending","disabled","onRun"])),y(m,{class:"btn sm ghost",to:`/deployments/${encodeURIComponent(S(b).id)}/logs?container=${encodeURIComponent(t.name)}`},{default:g(()=>[...e[31]||(e[31]=[l(" Logs ",-1)])]),_:1},8,["to"]),t.state==="running"&&!t.remote?(n(),k(m,{key:4,class:"btn sm ghost",to:`/deployments/${encodeURIComponent(S(b).id)}/terminal?container=${encodeURIComponent(t.name)}`},{default:g(()=>[...e[32]||(e[32]=[l(" Shell ",-1)])]),_:1},8,["to"])):u("",!0),t.state==="running"&&P(t)?(n(),o("a",{key:5,class:"btn sm ghost",href:P(t),target:"_blank",rel:"noreferrer"}," Open ",8,Ee)):u("",!0)])],4))),128))])])):(n(),o("p",Ve," No containers for this stack. If you expected some, check that their profile is one the spec selects — a service behind a profile that is not enabled is never started. "))])],64)):(n(),o("div",X,[...e[0]||(e[0]=[s("b",null,"Docker did not answer.",-1),s("p",null," Nothing below could be determined — this is not the same as nothing running. Check the daemon and the socket mount, then refresh. ",-1)])]))],64)):u("",!0)])}}}),Xe=G(Fe,[["__scopeId","data-v-80cf98f3"]]);export{Xe as default};
2
- //# sourceMappingURL=RuntimeTab-7GuemvVX.js.map
@@ -1 +0,0 @@
1
- .task[data-v-80cf98f3]{font-size:var(--t-sm);max-width:30ch}
@@ -1,2 +0,0 @@
1
- import{d as s,o as e,c as a,F as o,y as r,D as n,E as i}from"./index-BXPaPWhH.js";const c={role:"status","aria-live":"polite","aria-label":"loading"},p=s({__name:"SkeletonList",props:{rows:{default:3},tall:{type:Boolean,default:!1}},setup(l){return(d,u)=>(e(),a("div",c,[(e(!0),a(o,null,r(l.rows,t=>(e(),a("div",{key:t,class:i(["skel",{tall:l.tall}]),style:n({width:`${100-t*7}%`})},null,6))),128))]))}});export{p as _};
2
- //# sourceMappingURL=SkeletonList.vue_vue_type_script_setup_true_lang-DMdwApyt.js.map
@@ -1,2 +0,0 @@
1
- import{d as W,c as u,a as s,f as l,e as h,w as g,_ as D,z as S,g as p,F as A,t as x,i as n,I as q,v as i,y as F,Z as P,r as d,n as O,C as L,O as k,j as Y,o as r}from"./index-BXPaPWhH.js";import{_ as G}from"./ActionButton.vue_vue_type_script_setup_true_lang-CxT4O7XN.js";import{_ as K}from"./ErrorNote.vue_vue_type_script_setup_true_lang-B5-EJpIO.js";import{_ as Z}from"./HelpModal.vue_vue_type_script_setup_true_lang-DWDQ-wIa.js";import{_ as N}from"./SelectMenu.vue_vue_type_script_setup_true_lang-VAye0ofl.js";import{_ as J}from"./SkeletonList.vue_vue_type_script_setup_true_lang-DMdwApyt.js";import"./Teleport-DTF2H_ia.js";const Q={class:"page-head"},X={class:"sub"},_={class:"panel"},ee={class:"row",style:{gap:"var(--s3)","align-items":"center"}},se={class:"code",style:{flex:"1","overflow-x":"auto","white-space":"nowrap"}},oe={class:"panel"},te={class:"phead"},le={class:"check"},ae={class:"row",style:{gap:"var(--s4)","flex-wrap":"wrap"}},ne={class:"field inline"},ie={key:0,class:"field inline"},re={class:"field inline"},de={key:0,class:"hint"},ue={key:1,class:"field"},pe={class:"field"},ve={class:"field"},me={class:"hint"},ce={class:"field"},he={class:"field"},fe={class:"field"},ye={class:"field"},be={class:"field"},ge={class:"field"},ke=["placeholder"],we={key:3},Ue={class:"section",style:{"margin-top":"var(--s4)"}},xe={class:"row",style:{"flex-wrap":"wrap",gap:"var(--s3)"}},Ce=["for"],Ie=["id","onUpdate:modelValue"],Ve={class:"panel"},Re={class:"field"},Se={class:"field"},Ae={class:"field"},Oe={class:"row",style:{gap:"var(--s3)"}},Le={key:0,class:"mute",style:{"font-size":"var(--t-sm)","margin-top":"var(--s3)"}},$e=W({__name:"SsoView",setup(Ee){const E={subject:"sub",username:"preferred_username",email:"email",name:"name",avatar:"picture"},C=()=>({mode:"oidc",enabled:!0,label:"",clientId:"",discoveryUrl:"",provider:"github",authorizeUrl:"",tokenUrl:"",userInfoUrl:"",emailsUrl:"",groupsUrl:"",scopes:"",claimMap:{...E},allowedEmailDomains:[],allowedUsernames:[],requiredGroups:[],defaultRole:"admin"}),o=d(C()),m=d(""),f=d(""),y=d(""),b=d(""),w=d([]),I=d(""),c=d(!1),j=d(!1),v=d(""),U=d(!1);async function V(){const a=await O.get("/api/sso/config");if(j.value=!0,!a.ok){v.value=L(a,"read the sign-on settings");return}v.value="",w.value=a.body.presets??[],I.value=a.body.callbackUrl,c.value=a.body.configured,a.body.config&&(o.value={...C(),...a.body.config},f.value=(a.body.config.allowedEmailDomains??[]).join(", "),y.value=(a.body.config.allowedUsernames??[]).join(", "),b.value=(a.body.config.requiredGroups??[]).join(", ")),m.value=a.body.clientSecret}V();function T(a){o.value.provider=a;const e=w.value.find(t=>t.key===a);if(!e){o.value.authorizeUrl="",o.value.tokenUrl="",o.value.userInfoUrl="",o.value.emailsUrl="",o.value.groupsUrl="",o.value.claimMap={...E};return}o.value.authorizeUrl=e.authorizeUrl,o.value.tokenUrl=e.tokenUrl,o.value.userInfoUrl=e.userInfoUrl??"",o.value.emailsUrl="",o.value.groupsUrl="",o.value.scopes=e.scopes,o.value.claimMap={...e.claimMap},(!o.value.label||w.value.some(t=>t.label===o.value.label))&&(o.value.label=e.label)}const R=a=>a.split(",").map(e=>e.trim()).filter(Boolean);async function z(){U.value=!0;const a={...o.value,clientSecret:m.value,allowedEmailDomains:R(f.value),allowedUsernames:R(y.value),requiredGroups:R(b.value)},e=await O.put("/api/sso/config",a);if(U.value=!1,!e.ok){v.value=L(e,"save the provider"),k("error","Not saved. The server said why, at the top of this page.");return}v.value="",k("ok","Saved. The provider answered, so the settings are known good."),V()}async function M(){const a=await O.del("/api/sso/config");if(!a.ok){v.value=L(a,"remove the provider");return}k("ok","Removed. Existing accounts keep working."),o.value=C(),m.value="",f.value="",y.value="",b.value="",c.value=!1,V()}async function $(){try{await navigator.clipboard.writeText(I.value),k("ok","Copied.")}catch{k("error","Could not reach the clipboard — select the text and copy it.")}}const B=Y(()=>!o.value.clientId.trim()||!m.value?!1:o.value.mode==="oidc"?!!o.value.discoveryUrl.trim():!!o.value.authorizeUrl.trim()&&!!o.value.tokenUrl.trim());return(a,e)=>(r(),u("div",null,[s("div",Q,[s("div",null,[s("h1",null,[e[17]||(e[17]=l(" Single sign-on ",-1)),h(Z,{title:"How single sign-on works here"},{default:g(()=>[...e[16]||(e[16]=[s("p",null,[s("b",null,"This service is the relying party and nothing more."),l(" You register an OAuth or OIDC application in your own organisation and paste its client id and secret here. Your directory stays yours — no accounts are copied, and nothing is synchronised. ")],-1),s("p",null,[s("b",null,"Anyone who authenticates gets an account"),l(", created on their first sign-in. That is deliberate: your provider already decides who may authenticate (Workspace restricts to internal users, a GitHub app can be org-approved), and duplicating that policy here would just be a second list to keep in step. Narrow it with the sign-in rules below if you need to. ")],-1),s("p",null,[s("b",null,"Identity is the provider's subject, not the email."),l(" Someone changing their address keeps their account, their history and their tokens. An address is only used to adopt an account that already exists here, and only when the provider says it is verified. ")],-1),s("p",null,[s("b",null,"Nothing about local accounts changes."),l(" The admin credential, manually created users and personal API tokens all keep working exactly as they do now. ")],-1)])]),_:1})]),s("div",X,[e[19]||(e[19]=l(" Let people sign in with the identity provider you already run ",-1)),h(D,{label:"what is stored"},{default:g(()=>[...e[18]||(e[18]=[l(" One row: the endpoints, the client id, and the client secret. The secret has no read path — this page is shown a mask, and leaving it alone keeps what is stored. ",-1)])]),_:1})])])]),v.value?(r(),S(K,{key:0,text:v.value,title:"Single sign-on."},null,8,["text"])):p("",!0),j.value?(r(),u(A,{key:2},[s("section",_,[e[20]||(e[20]=s("div",{class:"phead"},[s("h2",{class:"section"},"Register this callback URL")],-1)),e[21]||(e[21]=s("p",{class:"dim"}," Paste this into your provider's application as the redirect / callback URL. It has to match exactly, and it is the same for every provider — a mismatch is the single most common reason the exchange fails, and the provider's error rarely says so. ",-1)),s("div",ee,[s("code",se,x(I.value),1),s("button",{onClick:$},"Copy")])]),s("section",oe,[s("div",te,[e[23]||(e[23]=s("h2",{class:"section"},"Provider",-1)),e[24]||(e[24]=s("span",{class:"grow"},null,-1)),s("label",le,[n(s("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>o.value.enabled=t),type:"checkbox"},null,512),[[q,o.value.enabled]]),e[22]||(e[22]=l(" Enabled",-1))])]),s("div",ae,[s("div",ne,[e[25]||(e[25]=s("label",{for:"sso-mode"},"Kind",-1)),h(N,{id:"sso-mode","model-value":o.value.mode,label:"Which protocol",options:[{value:"oidc",label:"OpenID Connect"},{value:"oauth2",label:"OAuth 2.0"}],"onUpdate:modelValue":e[1]||(e[1]=t=>o.value.mode=t)},null,8,["model-value"])]),o.value.mode==="oauth2"?(r(),u("div",ie,[e[26]||(e[26]=s("label",{for:"sso-preset"},"Provider",-1)),h(N,{id:"sso-preset","model-value":o.value.provider,label:"Which provider",options:[...w.value.map(t=>({value:t.key,label:t.label})),{value:"custom",label:"Custom"}],"onUpdate:modelValue":e[2]||(e[2]=t=>T(String(t)))},null,8,["model-value","options"])])):p("",!0),s("div",re,[e[27]||(e[27]=s("label",{for:"sso-label"},"Button text",-1)),n(s("input",{id:"sso-label","onUpdate:modelValue":e[3]||(e[3]=t=>o.value.label=t),type:"text",placeholder:"Acme SSO"},null,512),[[i,o.value.label]])])]),o.value.mode==="oidc"?(r(),u("p",de," Everything but the client id and secret is discovered from the issuer. GitHub is the one exception you may reach for and cannot use here: its OIDC document signs Actions job tokens, not user logins — use OAuth 2.0 for GitHub. ")):p("",!0),o.value.mode==="oidc"?(r(),u("div",ue,[e[28]||(e[28]=s("label",{for:"sso-issuer"},"Issuer",-1)),n(s("input",{id:"sso-issuer","onUpdate:modelValue":e[4]||(e[4]=t=>o.value.discoveryUrl=t),type:"text",spellcheck:"false",placeholder:"https://accounts.google.com"},null,512),[[i,o.value.discoveryUrl]]),e[29]||(e[29]=s("p",{class:"hint"},[l("Or the full "),s("code",null,"…/.well-known/openid-configuration"),l(" URL. It is fetched when you save, so a typo is refused here rather than at someone's first login.")],-1))])):p("",!0),s("div",pe,[e[30]||(e[30]=s("label",{for:"sso-client"},"Client id",-1)),n(s("input",{id:"sso-client","onUpdate:modelValue":e[5]||(e[5]=t=>o.value.clientId=t),type:"text",spellcheck:"false",autocomplete:"off"},null,512),[[i,o.value.clientId]])]),s("div",ve,[e[31]||(e[31]=s("label",{for:"sso-secret"},"Client secret",-1)),n(s("input",{id:"sso-secret","onUpdate:modelValue":e[6]||(e[6]=t=>m.value=t),type:"password",spellcheck:"false",autocomplete:"new-password"},null,512),[[i,m.value]]),s("p",me," Write-only. "+x(c.value?"A secret is stored — leave this as it is to keep it.":"Stored on save and never returned by any route."),1)]),o.value.mode==="oauth2"?(r(),u(A,{key:2},[s("div",ce,[e[32]||(e[32]=s("label",{for:"sso-auth"},"Authorize URL",-1)),n(s("input",{id:"sso-auth","onUpdate:modelValue":e[7]||(e[7]=t=>o.value.authorizeUrl=t),type:"text",spellcheck:"false"},null,512),[[i,o.value.authorizeUrl]])]),s("div",he,[e[33]||(e[33]=s("label",{for:"sso-token"},"Token URL",-1)),n(s("input",{id:"sso-token","onUpdate:modelValue":e[8]||(e[8]=t=>o.value.tokenUrl=t),type:"text",spellcheck:"false"},null,512),[[i,o.value.tokenUrl]])]),s("div",fe,[e[34]||(e[34]=s("label",{for:"sso-userinfo"},"User info URL",-1)),n(s("input",{id:"sso-userinfo","onUpdate:modelValue":e[9]||(e[9]=t=>o.value.userInfoUrl=t),type:"text",spellcheck:"false"},null,512),[[i,o.value.userInfoUrl]]),e[35]||(e[35]=s("p",{class:"hint"},"Leave empty to take the identity from the token response alone — a subject and nothing else.",-1))]),s("div",ye,[e[36]||(e[36]=s("label",{for:"sso-emails"},[l("Emails URL "),s("span",{class:"mute"},"(optional)")],-1)),n(s("input",{id:"sso-emails","onUpdate:modelValue":e[10]||(e[10]=t=>o.value.emailsUrl=t),type:"text",spellcheck:"false"},null,512),[[i,o.value.emailsUrl]]),e[37]||(e[37]=s("p",{class:"hint"}," Only consulted when the profile carries no address — which is GitHub's default. Filled in automatically for a preset, and needed only if you pointed the user info URL at your own host. ",-1))]),s("div",be,[e[38]||(e[38]=s("label",{for:"sso-groups-url"},[l("Groups URL "),s("span",{class:"mute"},"(optional)")],-1)),n(s("input",{id:"sso-groups-url","onUpdate:modelValue":e[11]||(e[11]=t=>o.value.groupsUrl=t),type:"text",spellcheck:"false"},null,512),[[i,o.value.groupsUrl]]),e[39]||(e[39]=s("p",{class:"hint"}," Where this host asks which groups someone is in. Only consulted when there is a group rule below. Filled in automatically for a preset — but only while the user info URL is still the preset's, because a self-hosted host's token must not be sent to the public one, so a self-hosted provider needs this typed. ",-1))])],64)):p("",!0),s("div",ge,[e[40]||(e[40]=s("label",{for:"sso-scopes"},"Scopes",-1)),n(s("input",{id:"sso-scopes","onUpdate:modelValue":e[12]||(e[12]=t=>o.value.scopes=t),type:"text",spellcheck:"false",placeholder:o.value.mode==="oidc"?"openid profile email":""},null,8,ke),[[i,o.value.scopes]])]),o.value.mode==="oauth2"?(r(),u("div",we,[s("h3",Ue,[e[42]||(e[42]=l(" Claim mapping ",-1)),h(D,{label:"what this is"},{default:g(()=>[...e[41]||(e[41]=[l(" Which key of the provider's user response holds each field. Flat lookups, not expressions. A preset fills these in; you only touch them for a provider nobody has written a preset for. ",-1)])]),_:1})]),s("div",xe,[(r(),u(A,null,F(["subject","username","email","name","avatar"],t=>s("div",{key:t,class:"field inline"},[s("label",{for:`cm-${t}`},x(t),9,Ce),n(s("input",{id:`cm-${t}`,"onUpdate:modelValue":H=>o.value.claimMap[t]=H,type:"text",spellcheck:"false",style:{width:"11rem"}},null,8,Ie),[[i,o.value.claimMap[t]]])])),64))])])):p("",!0)]),s("section",Ve,[e[49]||(e[49]=s("div",{class:"phead"},[s("h2",{class:"section"},"Who gets an account")],-1)),e[50]||(e[50]=s("p",{class:"dim"},[l(" By default, anyone your provider lets authenticate. It already owns that decision; this is only here for the case where the application you registered is broader than the people who should reach this control plane. "),s("b",null,"Any one entry in a list is enough to satisfy that list, and every list you fill in has to be satisfied"),l(" — fill all three in and a login needs a listed domain, a matching username, and one of the groups. A list left empty is not a rule at all: it lets everyone past that check, it does not lock everyone out. ")],-1)),s("div",Re,[e[43]||(e[43]=s("label",{for:"sso-domains"},"Allowed email domains",-1)),n(s("input",{id:"sso-domains","onUpdate:modelValue":e[13]||(e[13]=t=>f.value=t),type:"text",spellcheck:"false",placeholder:"example.com, corp.example"},null,512),[[i,f.value]]),e[44]||(e[44]=s("p",{class:"hint"},[l(" Comma-separated; subdomains count. Empty means no restriction. "),s("b",null,"Non-empty means a login with no email address at all is refused too"),l(" — a provider that hides the address (a private GitHub profile with no "),s("code",null,"user:email"),l(" scope) cannot be checked, so it is not waved through. ")],-1))]),s("div",Se,[e[45]||(e[45]=s("label",{for:"sso-usernames"},"Allowed usernames",-1)),n(s("input",{id:"sso-usernames","onUpdate:modelValue":e[14]||(e[14]=t=>y.value=t),type:"text",spellcheck:"false",placeholder:"octocat, qa-[0-9]*"},null,512),[[i,y.value]]),e[46]||(e[46]=P('<p class="hint"> Comma-separated patterns, case-insensitive: <code>*</code>, <code>?</code> and <code>[0-9]</code> classes. Empty means no restriction. <b>Non-empty means a login whose provider sends no username is refused too</b> — so this is a rule for a provider that actually has usernames (GitHub&#39;s <code>login</code>, GitLab&#39;s <code>username</code>). On an OpenID Connect issuer that sends no <code>preferred_username</code> it refuses everyone rather than nobody, so set it only where you know the provider sends one. </p>',1))]),s("div",Ae,[e[47]||(e[47]=s("label",{for:"sso-groups"},"Required groups",-1)),n(s("input",{id:"sso-groups","onUpdate:modelValue":e[15]||(e[15]=t=>b.value=t),type:"text",spellcheck:"false",placeholder:"acme, acme/backend"},null,512),[[i,b.value]]),e[48]||(e[48]=s("p",{class:"hint"},[l(" Comma-separated, and "),s("b",null,"exact names, not patterns"),l(" — an organisation login, or a group path like "),s("code",null,"acme/backend"),l(". Case-insensitive. Empty means no restriction. Non-empty makes every sign-in ask the provider for that person's groups, so it needs the OAuth scope that endpoint requires: "),s("b",null,"add it to Scopes above, or the save is refused"),l(" — and the refusal names the scope. It also needs a provider whose group list this host knows how to read, which a discovered OpenID Connect issuer never is; the save says so by name. If that call does not answer, the sign-in is refused rather than allowed. ")],-1))])]),s("div",Oe,[h(G,{variant:"primary",pending:U.value,disabled:U.value||!B.value,onClick:z},{default:g(()=>[l(x(c.value?"Save changes":"Enable single sign-on"),1)]),_:1},8,["pending","disabled"]),e[52]||(e[52]=s("span",{class:"grow"},null,-1)),c.value?(r(),S(G,{key:0,variant:"danger",confirm:`Remove ${o.value.label||"the provider"}?`,onRun:M},{default:g(()=>[...e[51]||(e[51]=[l(" Remove provider ",-1)])]),_:1},8,["confirm"])):p("",!0)]),c.value?(r(),u("p",Le," Removing the provider does not delete anyone. Those accounts keep their personal tokens, and pointing the same provider back at this host re-links them by subject. ")):p("",!0)],64)):(r(),S(J,{key:1,rows:5}))]))}});export{$e as default};
2
- //# sourceMappingURL=SsoView-5Kk12W1K.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SsoView-5Kk12W1K.js","sources":["../../src/views/SsoView.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Single sign-on: point this control plane at the identity provider the operator already runs.\n *\n * THE POINT OF THE PAGE is that nobody has to create accounts here any more. Whoever can\n * authenticate against the operator's directory can sign in, and the account appears on first\n * login. The provider keeps owning who is allowed — Google Workspace restricts to internal users,\n * a GitHub OAuth app can be org-approved — and this page deliberately does not rebuild that policy.\n *\n * TWO THINGS THIS FORM MUST NOT GET WRONG:\n *\n * 1. The callback URL is READ-ONLY and comes from the server. It has to match what is registered\n * on the provider's side byte for byte, and a value typed here (or derived from\n * `location.origin`) would drift from the one the flow actually sends the moment the UI is\n * reached by a second hostname.\n * 2. The client secret is write-only. The server answers with a mask; submitting the mask back\n * unchanged keeps the stored secret, so an operator editing the label cannot silently wipe it.\n */\nimport { computed, ref } from 'vue';\nimport { api, problem } from '../api/client';\nimport type { ClaimMap, SsoConfig, SsoConfigResponse, SsoPreset } from '../api/types';\nimport { toast } from '../composables/useToasts';\nimport ActionButton from '../components/ActionButton.vue';\nimport ErrorNote from '../components/ErrorNote.vue';\nimport HelpModal from '../components/HelpModal.vue';\nimport InfoHint from '../components/InfoHint.vue';\nimport SelectMenu from '../components/SelectMenu.vue';\nimport SkeletonList from '../components/SkeletonList.vue';\n\nconst OIDC_CLAIMS: ClaimMap = { subject: 'sub', username: 'preferred_username', email: 'email', name: 'name', avatar: 'picture' };\n\nconst blank = (): SsoConfig => ({\n mode: 'oidc',\n enabled: true,\n label: '',\n clientId: '',\n discoveryUrl: '',\n provider: 'github',\n authorizeUrl: '',\n tokenUrl: '',\n userInfoUrl: '',\n emailsUrl: '',\n groupsUrl: '',\n scopes: '',\n claimMap: { ...OIDC_CLAIMS },\n allowedEmailDomains: [],\n allowedUsernames: [],\n requiredGroups: [],\n defaultRole: 'admin',\n});\n\nconst form = ref<SsoConfig>(blank());\nconst secret = ref('');\n// The three rule lists are edited as comma-separated text, split on save. A `string[]` bound to an\n// input cannot hold \"acme,\" while someone is still typing the second entry.\nconst domains = ref('');\nconst usernames = ref('');\nconst groups = ref('');\nconst presets = ref<SsoPreset[]>([]);\nconst callbackUrl = ref('');\nconst configured = ref(false);\nconst loaded = ref(false);\nconst error = ref('');\nconst saving = ref(false);\n\nasync function load(): Promise<void> {\n const r = await api.get<SsoConfigResponse>('/api/sso/config');\n loaded.value = true;\n if (!r.ok) {\n error.value = problem(r, 'read the sign-on settings');\n return;\n }\n error.value = '';\n presets.value = r.body.presets ?? [];\n callbackUrl.value = r.body.callbackUrl;\n configured.value = r.body.configured;\n if (r.body.config) {\n form.value = { ...blank(), ...r.body.config };\n domains.value = (r.body.config.allowedEmailDomains ?? []).join(', ');\n usernames.value = (r.body.config.allowedUsernames ?? []).join(', ');\n groups.value = (r.body.config.requiredGroups ?? []).join(', ');\n }\n // The mask, so leaving the field alone re-submits it and the server keeps what it has.\n secret.value = r.body.clientSecret;\n}\nvoid load();\n\n/** Picking a preset fills the endpoints in. Everything stays editable — a self-hosted GitLab is\n * this preset with three URLs replaced, not a `custom` provider with five fields typed by hand. */\nfunction applyPreset(key: string): void {\n form.value.provider = key;\n const p = presets.value.find((x) => x.key === key);\n if (!p) {\n form.value.authorizeUrl = '';\n form.value.tokenUrl = '';\n form.value.userInfoUrl = '';\n form.value.emailsUrl = '';\n form.value.groupsUrl = '';\n form.value.claimMap = { ...OIDC_CLAIMS };\n return;\n }\n form.value.authorizeUrl = p.authorizeUrl;\n form.value.tokenUrl = p.tokenUrl;\n form.value.userInfoUrl = p.userInfoUrl ?? '';\n // Left blank on purpose: the server fills the preset's own emails and groups endpoints back in,\n // and only while userInfoUrl is still the preset's.\n form.value.emailsUrl = '';\n form.value.groupsUrl = '';\n form.value.scopes = p.scopes;\n form.value.claimMap = { ...p.claimMap };\n if (!form.value.label || presets.value.some((x) => x.label === form.value.label)) form.value.label = p.label;\n}\n\nconst list = (s: string): string[] => s.split(',').map((v) => v.trim()).filter(Boolean);\n\nasync function save(): Promise<void> {\n saving.value = true;\n // Annotated, not inline: `api.put` takes `unknown`, so without a type here a mistyped key\n // (`allowedUsernams`) would compile, the server would see no such field, the rule would silently\n // restrict nobody, and the form would say \"Saved\".\n const body: SsoConfig & { clientSecret: string } = {\n ...form.value,\n clientSecret: secret.value,\n allowedEmailDomains: list(domains.value),\n allowedUsernames: list(usernames.value),\n requiredGroups: list(groups.value),\n };\n const r = await api.put<{ ok: boolean; callbackUrl: string }>('/api/sso/config', body);\n saving.value = false;\n if (!r.ok) {\n error.value = problem(r, 'save the provider');\n // The banner is above the panels and this button is at the bottom of a long form, so a refusal\n // — the \"requiredGroups needs the … scope\" one especially — otherwise lands off-screen and\n // reads as the button having done nothing.\n toast('error', 'Not saved. The server said why, at the top of this page.');\n return;\n }\n error.value = '';\n toast('ok', 'Saved. The provider answered, so the settings are known good.');\n void load();\n}\n\nasync function remove(): Promise<void> {\n const r = await api.del('/api/sso/config');\n if (!r.ok) {\n error.value = problem(r, 'remove the provider');\n return;\n }\n toast('ok', 'Removed. Existing accounts keep working.');\n form.value = blank();\n secret.value = '';\n domains.value = '';\n usernames.value = '';\n groups.value = '';\n configured.value = false;\n void load();\n}\n\nasync function copyCallback(): Promise<void> {\n try {\n await navigator.clipboard.writeText(callbackUrl.value);\n toast('ok', 'Copied.');\n } catch {\n toast('error', 'Could not reach the clipboard — select the text and copy it.');\n }\n}\n\nconst canSave = computed(() => {\n if (!form.value.clientId.trim() || !secret.value) return false;\n if (form.value.mode === 'oidc') return !!form.value.discoveryUrl.trim();\n return !!form.value.authorizeUrl.trim() && !!form.value.tokenUrl.trim();\n});\n</script>\n\n<template>\n <div>\n <div class=\"page-head\">\n <div>\n <h1>\n Single sign-on\n <HelpModal title=\"How single sign-on works here\">\n <p>\n <b>This service is the relying party and nothing more.</b> You register an OAuth or\n OIDC application in your own organisation and paste its client id and secret here. Your\n directory stays yours — no accounts are copied, and nothing is synchronised.\n </p>\n <p>\n <b>Anyone who authenticates gets an account</b>, created on their first sign-in. That is\n deliberate: your provider already decides who may authenticate (Workspace restricts to\n internal users, a GitHub app can be org-approved), and duplicating that policy here\n would just be a second list to keep in step. Narrow it with the sign-in rules below if\n you need to.\n </p>\n <p>\n <b>Identity is the provider's subject, not the email.</b> Someone changing their\n address keeps their account, their history and their tokens. An address is only used to\n adopt an account that already exists here, and only when the provider says it is\n verified.\n </p>\n <p>\n <b>Nothing about local accounts changes.</b> The admin credential, manually created\n users and personal API tokens all keep working exactly as they do now.\n </p>\n </HelpModal>\n </h1>\n <div class=\"sub\">\n Let people sign in with the identity provider you already run\n <InfoHint label=\"what is stored\">\n One row: the endpoints, the client id, and the client secret. The secret has no read\n path — this page is shown a mask, and leaving it alone keeps what is stored.\n </InfoHint>\n </div>\n </div>\n </div>\n\n <ErrorNote v-if=\"error\" :text=\"error\" title=\"Single sign-on.\" />\n <SkeletonList v-if=\"!loaded\" :rows=\"5\" />\n\n <template v-else>\n <!-- ============================ the callback URL ============================ -->\n <section class=\"panel\">\n <div class=\"phead\"><h2 class=\"section\">Register this callback URL</h2></div>\n <p class=\"dim\">\n Paste this into your provider's application as the redirect / callback URL. It has to match\n exactly, and it is the same for every provider — a mismatch is the single most common\n reason the exchange fails, and the provider's error rarely says so.\n </p>\n <div class=\"row\" style=\"gap: var(--s3); align-items: center\">\n <code class=\"code\" style=\"flex: 1; overflow-x: auto; white-space: nowrap\">{{ callbackUrl }}</code>\n <button @click=\"copyCallback\">Copy</button>\n </div>\n </section>\n\n <!-- ============================ the provider ============================ -->\n <section class=\"panel\">\n <div class=\"phead\">\n <h2 class=\"section\">Provider</h2>\n <span class=\"grow\" />\n <label class=\"check\"><input v-model=\"form.enabled\" type=\"checkbox\" /> Enabled</label>\n </div>\n\n <div class=\"row\" style=\"gap: var(--s4); flex-wrap: wrap\">\n <div class=\"field inline\">\n <label for=\"sso-mode\">Kind</label>\n <SelectMenu\n id=\"sso-mode\"\n :model-value=\"form.mode\"\n label=\"Which protocol\"\n :options=\"[\n { value: 'oidc', label: 'OpenID Connect' },\n { value: 'oauth2', label: 'OAuth 2.0' },\n ]\"\n @update:model-value=\"(v) => (form.mode = v as 'oidc' | 'oauth2')\"\n />\n </div>\n <div v-if=\"form.mode === 'oauth2'\" class=\"field inline\">\n <label for=\"sso-preset\">Provider</label>\n <SelectMenu\n id=\"sso-preset\"\n :model-value=\"form.provider\"\n label=\"Which provider\"\n :options=\"[...presets.map((p) => ({ value: p.key, label: p.label })), { value: 'custom', label: 'Custom' }]\"\n @update:model-value=\"(v) => applyPreset(String(v))\"\n />\n </div>\n <div class=\"field inline\">\n <label for=\"sso-label\">Button text</label>\n <input id=\"sso-label\" v-model=\"form.label\" type=\"text\" placeholder=\"Acme SSO\" />\n </div>\n </div>\n\n <p v-if=\"form.mode === 'oidc'\" class=\"hint\">\n Everything but the client id and secret is discovered from the issuer. GitHub is the one\n exception you may reach for and cannot use here: its OIDC document signs Actions job\n tokens, not user logins — use OAuth 2.0 for GitHub.\n </p>\n\n <div v-if=\"form.mode === 'oidc'\" class=\"field\">\n <label for=\"sso-issuer\">Issuer</label>\n <input id=\"sso-issuer\" v-model=\"form.discoveryUrl\" type=\"text\" spellcheck=\"false\" placeholder=\"https://accounts.google.com\" />\n <p class=\"hint\">Or the full <code>…/.well-known/openid-configuration</code> URL. It is fetched when you save, so a typo is refused here rather than at someone's first login.</p>\n </div>\n\n <div class=\"field\">\n <label for=\"sso-client\">Client id</label>\n <input id=\"sso-client\" v-model=\"form.clientId\" type=\"text\" spellcheck=\"false\" autocomplete=\"off\" />\n </div>\n <div class=\"field\">\n <label for=\"sso-secret\">Client secret</label>\n <input id=\"sso-secret\" v-model=\"secret\" type=\"password\" spellcheck=\"false\" autocomplete=\"new-password\" />\n <p class=\"hint\">\n Write-only. {{ configured ? 'A secret is stored — leave this as it is to keep it.' : 'Stored on save and never returned by any route.' }}\n </p>\n </div>\n\n <template v-if=\"form.mode === 'oauth2'\">\n <div class=\"field\">\n <label for=\"sso-auth\">Authorize URL</label>\n <input id=\"sso-auth\" v-model=\"form.authorizeUrl\" type=\"text\" spellcheck=\"false\" />\n </div>\n <div class=\"field\">\n <label for=\"sso-token\">Token URL</label>\n <input id=\"sso-token\" v-model=\"form.tokenUrl\" type=\"text\" spellcheck=\"false\" />\n </div>\n <div class=\"field\">\n <label for=\"sso-userinfo\">User info URL</label>\n <input id=\"sso-userinfo\" v-model=\"form.userInfoUrl\" type=\"text\" spellcheck=\"false\" />\n <p class=\"hint\">Leave empty to take the identity from the token response alone — a subject and nothing else.</p>\n </div>\n <div class=\"field\">\n <label for=\"sso-emails\">Emails URL <span class=\"mute\">(optional)</span></label>\n <input id=\"sso-emails\" v-model=\"form.emailsUrl\" type=\"text\" spellcheck=\"false\" />\n <p class=\"hint\">\n Only consulted when the profile carries no address — which is GitHub's default. Filled\n in automatically for a preset, and needed only if you pointed the user info URL at your\n own host.\n </p>\n </div>\n <div class=\"field\">\n <label for=\"sso-groups-url\">Groups URL <span class=\"mute\">(optional)</span></label>\n <input id=\"sso-groups-url\" v-model=\"form.groupsUrl\" type=\"text\" spellcheck=\"false\" />\n <p class=\"hint\">\n Where this host asks which groups someone is in. Only consulted when there is a group\n rule below. Filled in automatically for a preset — but only while the user info URL is\n still the preset's, because a self-hosted host's token must not be sent to the public\n one, so a self-hosted provider needs this typed.\n </p>\n </div>\n </template>\n\n <div class=\"field\">\n <label for=\"sso-scopes\">Scopes</label>\n <input id=\"sso-scopes\" v-model=\"form.scopes\" type=\"text\" spellcheck=\"false\" :placeholder=\"form.mode === 'oidc' ? 'openid profile email' : ''\" />\n </div>\n\n <div v-if=\"form.mode === 'oauth2'\">\n <h3 class=\"section\" style=\"margin-top: var(--s4)\">\n Claim mapping\n <InfoHint label=\"what this is\">\n Which key of the provider's user response holds each field. Flat lookups, not\n expressions. A preset fills these in; you only touch them for a provider nobody has\n written a preset for.\n </InfoHint>\n </h3>\n <div class=\"row\" style=\"flex-wrap: wrap; gap: var(--s3)\">\n <div v-for=\"k in (['subject', 'username', 'email', 'name', 'avatar'] as const)\" :key=\"k\" class=\"field inline\">\n <label :for=\"`cm-${k}`\">{{ k }}</label>\n <input :id=\"`cm-${k}`\" v-model=\"form.claimMap[k]\" type=\"text\" spellcheck=\"false\" style=\"width: 11rem\" />\n </div>\n </div>\n </div>\n </section>\n\n <!-- ============================ who gets in ============================ -->\n <section class=\"panel\">\n <div class=\"phead\"><h2 class=\"section\">Who gets an account</h2></div>\n <p class=\"dim\">\n By default, anyone your provider lets authenticate. It already owns that decision; this is\n only here for the case where the application you registered is broader than the people who\n should reach this control plane. <b>Any one entry in a list is enough to satisfy that\n list, and every list you fill in has to be satisfied</b> — fill all three in and a login\n needs a listed domain, a matching username, and one of the groups. A list left empty is not\n a rule at all: it lets everyone past that check, it does not lock everyone out.\n </p>\n <div class=\"field\">\n <label for=\"sso-domains\">Allowed email domains</label>\n <input id=\"sso-domains\" v-model=\"domains\" type=\"text\" spellcheck=\"false\" placeholder=\"example.com, corp.example\" />\n <p class=\"hint\">\n Comma-separated; subdomains count. Empty means no restriction. <b>Non-empty means a login\n with no email address at all is refused too</b> — a provider that hides the address (a\n private GitHub profile with no <code>user:email</code> scope) cannot be checked, so it is\n not waved through.\n </p>\n </div>\n\n <div class=\"field\">\n <label for=\"sso-usernames\">Allowed usernames</label>\n <input id=\"sso-usernames\" v-model=\"usernames\" type=\"text\" spellcheck=\"false\" placeholder=\"octocat, qa-[0-9]*\" />\n <p class=\"hint\">\n Comma-separated patterns, case-insensitive: <code>*</code>, <code>?</code> and\n <code>[0-9]</code> classes. Empty means no restriction. <b>Non-empty means a login whose\n provider sends no username is refused too</b> — so this is a rule for a provider that\n actually has usernames (GitHub's <code>login</code>, GitLab's <code>username</code>). On\n an OpenID Connect issuer that sends no <code>preferred_username</code> it refuses\n everyone rather than nobody, so set it only where you know the provider sends one.\n </p>\n </div>\n\n <div class=\"field\">\n <label for=\"sso-groups\">Required groups</label>\n <input id=\"sso-groups\" v-model=\"groups\" type=\"text\" spellcheck=\"false\" placeholder=\"acme, acme/backend\" />\n <p class=\"hint\">\n Comma-separated, and <b>exact names, not patterns</b> — an organisation login, or a group\n path like <code>acme/backend</code>. Case-insensitive. Empty means no restriction.\n Non-empty makes every sign-in ask the provider for that person's groups, so it needs the\n OAuth scope that endpoint requires: <b>add it to Scopes above, or the save is refused</b>\n — and the refusal names the scope. It also needs a provider whose group list this host\n knows how to read, which a discovered OpenID Connect issuer never is; the save says so by\n name. If that call does not answer, the sign-in is refused rather than allowed.\n </p>\n </div>\n </section>\n\n <div class=\"row\" style=\"gap: var(--s3)\">\n <ActionButton variant=\"primary\" :pending=\"saving\" :disabled=\"saving || !canSave\" @click=\"save\">\n {{ configured ? 'Save changes' : 'Enable single sign-on' }}\n </ActionButton>\n <span class=\"grow\" />\n <ActionButton v-if=\"configured\" variant=\"danger\" :confirm=\"`Remove ${form.label || 'the provider'}?`\" @run=\"remove\">\n Remove provider\n </ActionButton>\n </div>\n <p v-if=\"configured\" class=\"mute\" style=\"font-size: var(--t-sm); margin-top: var(--s3)\">\n Removing the provider does not delete anyone. Those accounts keep their personal tokens, and\n pointing the same provider back at this host re-links them by subject.\n </p>\n </template>\n </div>\n</template>\n"],"names":["OIDC_CLAIMS","blank","form","ref","secret","domains","usernames","groups","presets","callbackUrl","configured","loaded","error","saving","load","r","api","problem","applyPreset","key","p","x","list","s","v","save","body","toast","remove","copyCallback","canSave","computed","_createElementBlock","_createElementVNode","_hoisted_1","_createVNode","HelpModal","_cache","_hoisted_2","InfoHint","_createBlock","ErrorNote","_Fragment","_hoisted_3","_hoisted_4","_hoisted_5","_toDisplayString","_hoisted_6","_hoisted_7","_hoisted_8","$event","_vModelCheckbox","_hoisted_9","_hoisted_10","SelectMenu","_openBlock","_hoisted_11","_hoisted_12","_vModelText","_hoisted_13","_hoisted_14","_hoisted_15","_hoisted_16","_hoisted_17","_hoisted_18","_hoisted_19","_hoisted_20","_hoisted_21","_hoisted_22","_hoisted_23","_hoisted_25","_hoisted_26","_hoisted_27","_renderList","k","_hoisted_28","_hoisted_30","_hoisted_31","_hoisted_32","_hoisted_33","_hoisted_34","ActionButton","_hoisted_35","SkeletonList"],"mappings":"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"}