@samyx/preview-stacks-ui 0.2.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/assets/ActionButton.vue_vue_type_script_setup_true_lang-eyfGLbs3.js +2 -0
  4. package/dist/assets/ActionButton.vue_vue_type_script_setup_true_lang-eyfGLbs3.js.map +1 -0
  5. package/dist/assets/AxesTab-BRrPVCft.js +3 -0
  6. package/dist/assets/AxesTab-BRrPVCft.js.map +1 -0
  7. package/dist/assets/ConfigTab-BOCoIOPW.js +2 -0
  8. package/dist/assets/ConfigTab-BOCoIOPW.js.map +1 -0
  9. package/dist/assets/DangerTab-dMsIXX7F.js +2 -0
  10. package/dist/assets/DangerTab-dMsIXX7F.js.map +1 -0
  11. package/dist/assets/DashboardView-D27l8yqd.js +2 -0
  12. package/dist/assets/DashboardView-D27l8yqd.js.map +1 -0
  13. package/dist/assets/DeploymentDetailView-BjELdE9f.js +2 -0
  14. package/dist/assets/DeploymentDetailView-BjELdE9f.js.map +1 -0
  15. package/dist/assets/DeploymentsView-02Cg08fs.js +2 -0
  16. package/dist/assets/DeploymentsView-02Cg08fs.js.map +1 -0
  17. package/dist/assets/ErrorNote.vue_vue_type_script_setup_true_lang-vu8RudtR.js +2 -0
  18. package/dist/assets/ErrorNote.vue_vue_type_script_setup_true_lang-vu8RudtR.js.map +1 -0
  19. package/dist/assets/JobDetailView-Ck89gkKn.js +2 -0
  20. package/dist/assets/JobDetailView-Ck89gkKn.js.map +1 -0
  21. package/dist/assets/JobsView-0OjmomhQ.js +2 -0
  22. package/dist/assets/JobsView-0OjmomhQ.js.map +1 -0
  23. package/dist/assets/LogsTab-Bb2E4F-n.js +4 -0
  24. package/dist/assets/LogsTab-Bb2E4F-n.js.map +1 -0
  25. package/dist/assets/NotFoundView-3kRJpFYC.js +2 -0
  26. package/dist/assets/NotFoundView-3kRJpFYC.js.map +1 -0
  27. package/dist/assets/OverviewTab-BrElTGDU.js +2 -0
  28. package/dist/assets/OverviewTab-BrElTGDU.js.map +1 -0
  29. package/dist/assets/RequiresTab-DB0yEAmP.js +2 -0
  30. package/dist/assets/RequiresTab-DB0yEAmP.js.map +1 -0
  31. package/dist/assets/SettingsView-CZvKTBtQ.js +2 -0
  32. package/dist/assets/SettingsView-CZvKTBtQ.js.map +1 -0
  33. package/dist/assets/SkeletonList.vue_vue_type_script_setup_true_lang-ECJq9map.js +2 -0
  34. package/dist/assets/SkeletonList.vue_vue_type_script_setup_true_lang-ECJq9map.js.map +1 -0
  35. package/dist/assets/StateBadge.vue_vue_type_script_setup_true_lang-BJu6FAcJ.js +2 -0
  36. package/dist/assets/StateBadge.vue_vue_type_script_setup_true_lang-BJu6FAcJ.js.map +1 -0
  37. package/dist/assets/SubmitView-dblBZxuc.js +45 -0
  38. package/dist/assets/SubmitView-dblBZxuc.js.map +1 -0
  39. package/dist/assets/VarEditor.vue_vue_type_script_setup_true_lang-DYi-LMaK.js +2 -0
  40. package/dist/assets/VarEditor.vue_vue_type_script_setup_true_lang-DYi-LMaK.js.map +1 -0
  41. package/dist/assets/index--8-Mcu3f.js +29 -0
  42. package/dist/assets/index--8-Mcu3f.js.map +1 -0
  43. package/dist/assets/index-C_dmDOA0.css +1 -0
  44. package/dist/assets/useDeployment-ByQUNe_X.js +2 -0
  45. package/dist/assets/useDeployment-ByQUNe_X.js.map +1 -0
  46. package/dist/assets/useFormat-CNPlQbIu.js +2 -0
  47. package/dist/assets/useFormat-CNPlQbIu.js.map +1 -0
  48. package/dist/assets/useVars-VN9gYdR6.js +2 -0
  49. package/dist/assets/useVars-VN9gYdR6.js.map +1 -0
  50. package/dist/index.html +33 -0
  51. package/nginx.conf +52 -0
  52. package/package.json +32 -0
@@ -0,0 +1 @@
1
+ :root,:root[data-theme=dark]{color-scheme:dark;--bg: #0b0d10;--bg-grad: radial-gradient(1200px 600px at 15% -10%, #141a24 0%, #0b0d10 60%);--panel: #14171c;--panel-2: #191d24;--sunk: #090b0e;--line: #252b34;--line-soft: #1c212a;--fg: #e7ebf1;--fg-dim: #9aa4b2;--fg-mute: #6d7683;--accent: #5b9dff;--accent-soft: rgba(91, 157, 255, .14);--ok: #3fb950;--ok-soft: rgba(63, 185, 80, .13);--fail: #f85149;--fail-soft: rgba(248, 81, 73, .13);--leak: #e3922a;--leak-soft: rgba(227, 146, 42, .14);--neutral: #8b949e;--neutral-soft: rgba(139, 148, 158, .13);--shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .28);--skel: linear-gradient(90deg, #1a1e25 0%, #232833 50%, #1a1e25 100%)}@media(prefers-color-scheme:light){:root:not([data-theme=dark]){color-scheme:light;--bg: #f6f7f9;--bg-grad: radial-gradient(1200px 600px at 15% -10%, #ffffff 0%, #f6f7f9 60%);--panel: #ffffff;--panel-2: #f3f5f8;--sunk: #f0f2f5;--line: #dde1e7;--line-soft: #e8ebef;--fg: #1a1d22;--fg-dim: #5a6472;--fg-mute: #838d9a;--accent: #1f6feb;--accent-soft: rgba(31, 111, 235, .1);--ok: #1a7f37;--ok-soft: rgba(26, 127, 55, .1);--fail: #cf222e;--fail-soft: rgba(207, 34, 46, .1);--leak: #9a6700;--leak-soft: rgba(154, 103, 0, .12);--neutral: #656d76;--neutral-soft: rgba(101, 109, 118, .1);--shadow: 0 1px 2px rgba(16, 22, 35, .06), 0 8px 24px rgba(16, 22, 35, .08);--skel: linear-gradient(90deg, #eceff3 0%, #f7f9fb 50%, #eceff3 100%)}}:root[data-theme=light]{color-scheme:light;--bg: #f6f7f9;--bg-grad: radial-gradient(1200px 600px at 15% -10%, #ffffff 0%, #f6f7f9 60%);--panel: #ffffff;--panel-2: #f3f5f8;--sunk: #f0f2f5;--line: #dde1e7;--line-soft: #e8ebef;--fg: #1a1d22;--fg-dim: #5a6472;--fg-mute: #838d9a;--accent: #1f6feb;--accent-soft: rgba(31, 111, 235, .1);--ok: #1a7f37;--ok-soft: rgba(26, 127, 55, .1);--fail: #cf222e;--fail-soft: rgba(207, 34, 46, .1);--leak: #9a6700;--leak-soft: rgba(154, 103, 0, .12);--neutral: #656d76;--neutral-soft: rgba(101, 109, 118, .1);--shadow: 0 1px 2px rgba(16, 22, 35, .06), 0 8px 24px rgba(16, 22, 35, .08);--skel: linear-gradient(90deg, #eceff3 0%, #f7f9fb 50%, #eceff3 100%)}:root{--t-xs: 11px;--t-sm: 12.5px;--t-md: 14px;--t-lg: 16px;--t-xl: 20px;--t-2xl: 26px;--t-3xl: 34px;--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;--s1: 4px;--s2: 8px;--s3: 12px;--s4: 16px;--s5: 24px;--s6: 32px;--s7: 48px;--r1: 6px;--r2: 10px;--r3: 14px;--rail: 208px;--dur-fast: .12s;--dur: .18s;--dur-slow: .28s;--ease: cubic-bezier(.2, .7, .3, 1);--stagger-step: 28ms}@media(prefers-reduced-motion:reduce){:root:not([data-motion=full]){--dur-fast: 0ms;--dur: 0ms;--dur-slow: 0ms;--stagger-step: 0ms}:root:not([data-motion=full]) *,:root:not([data-motion=full]) *:before,:root:not([data-motion=full]) *:after{animation-duration:1ms!important;animation-iteration-count:1!important;transition-duration:1ms!important;scroll-behavior:auto!important}}:root[data-motion=none]{--dur-fast: 0ms;--dur: 0ms;--dur-slow: 0ms;--stagger-step: 0ms}:root[data-motion=none] *,:root[data-motion=none] *:before,:root[data-motion=none] *:after{animation-duration:1ms!important;animation-iteration-count:1!important;transition-duration:1ms!important;scroll-behavior:auto!important}*,*:before,*:after{box-sizing:border-box}html,body,#app{height:100%}body{margin:0;background:var(--bg);background-image:var(--bg-grad);background-attachment:fixed;color:var(--fg);font:var(--t-md) / 1.55 var(--sans);-webkit-font-smoothing:antialiased;overflow-x:hidden}h1,h2,h3,p,ul,figure{margin:0}ul{padding:0;list-style:none}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--mono);font-size:.92em}:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:3px}.skip{position:absolute;left:-9999px;top:0;z-index:100;padding:var(--s2) var(--s3);background:var(--panel);border:1px solid var(--line);border-radius:var(--r1)}.skip:focus{left:var(--s3);top:var(--s3)}.shell{display:grid;grid-template-columns:var(--rail) minmax(0,1fr);min-height:100%}.rail{border-right:1px solid var(--line);background:color-mix(in srgb,var(--panel) 70%,transparent);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);padding:var(--s4) var(--s3);position:sticky;top:0;height:100vh;display:flex;flex-direction:column;gap:var(--s4)}.brand{display:flex;align-items:center;gap:var(--s2);font-size:var(--t-lg);font-weight:650;letter-spacing:-.01em;color:var(--fg)}.brand:hover{text-decoration:none}.brand svg{color:var(--accent)}.rail nav{display:flex;flex-direction:column;gap:2px}.navlink{display:flex;align-items:center;gap:var(--s3);padding:var(--s2) var(--s3);border-radius:var(--r1);color:var(--fg-dim);font-weight:500;transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}.navlink:hover{background:var(--panel-2);color:var(--fg);text-decoration:none}.navlink.router-link-active{background:var(--accent-soft);color:var(--accent)}.navlink svg{flex:none}.navlink .count{margin-left:auto;font:500 var(--t-xs) / 1 var(--mono);color:var(--fg-mute);background:var(--panel-2);padding:3px 6px;border-radius:999px}.rail .foot{margin-top:auto;font-size:var(--t-xs);color:var(--fg-mute);display:grid;gap:var(--s1);word-break:break-all}.view{min-width:0;padding:var(--s5) var(--s5) var(--s7);max-width:1180px}@media(max-width:800px){.shell{grid-template-columns:minmax(0,1fr)}.rail{position:fixed;inset:auto 0 0 0;height:auto;flex-direction:row;align-items:center;gap:var(--s1);padding:var(--s2) var(--s2) calc(var(--s2) + env(safe-area-inset-bottom));border-right:0;border-top:1px solid var(--line);background:color-mix(in srgb,var(--panel) 92%,transparent);z-index:20}.rail .brand,.rail .foot{display:none}.rail nav{flex-direction:row;flex:1;justify-content:space-around}.navlink{flex-direction:column;gap:2px;font-size:var(--t-xs);padding:var(--s1) var(--s2);min-width:56px;align-items:center}.navlink .count{display:none}.view{padding:var(--s4) var(--s4) calc(84px + env(safe-area-inset-bottom))}}.page-head{display:flex;align-items:flex-start;gap:var(--s3);flex-wrap:wrap;margin-bottom:var(--s5)}.page-head h1{font-size:var(--t-2xl);font-weight:650;letter-spacing:-.02em;line-height:1.2}.page-head .sub{color:var(--fg-dim);font-size:var(--t-sm);margin-top:2px}.grow{flex:1}h2.section{font-size:var(--t-xs);text-transform:uppercase;letter-spacing:.09em;color:var(--fg-dim);font-weight:600}h3{font-size:var(--t-md);font-weight:600}.dim{color:var(--fg-dim)}.mute{color:var(--fg-mute)}.nowrap{white-space:nowrap}.break{word-break:break-all}.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--r3);padding:var(--s4) var(--s4) var(--s5);margin-bottom:var(--s4);min-width:0}.panel>.phead{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap;margin-bottom:var(--s3)}.grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:var(--s4);align-items:start}.row{display:flex;gap:var(--s2);align-items:center;flex-wrap:wrap}button,.btn{font:500 var(--t-sm) / 1 var(--sans);color:var(--fg);background:var(--panel-2);border:1px solid var(--line);border-radius:var(--r1);padding:var(--s2) var(--s3);min-height:32px;cursor:pointer;display:inline-flex;align-items:center;gap:var(--s2);position:relative;overflow:hidden;transition:background var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease),transform var(--dur-fast) var(--ease)}button:hover:not(:disabled),.btn:hover{border-color:var(--fg-mute);text-decoration:none}button:active:not(:disabled){transform:translateY(1px)}button:disabled{opacity:.45;cursor:not-allowed}button.primary{background:var(--accent);border-color:var(--accent);color:#fff}button.danger{background:var(--fail-soft);border-color:color-mix(in srgb,var(--fail) 45%,transparent);color:var(--fail)}button.ghost{background:transparent}button.sm{min-height:27px;padding:var(--s1) var(--s2);font-size:var(--t-xs)}button .progress{position:absolute;inset:auto 0 0 0;height:2px;background:currentColor;opacity:.65;animation:indeterminate 1.1s var(--ease) infinite;transform-origin:left}input[type=text],input[type=password],input[type=search],textarea,select{font:var(--t-sm) / 1.5 var(--sans);color:var(--fg);background:var(--sunk);border:1px solid var(--line);border-radius:var(--r1);padding:var(--s2) var(--s3);min-height:32px;width:100%;min-width:0}textarea{font-family:var(--mono);font-size:var(--t-sm);line-height:1.55;resize:vertical;white-space:pre;overflow-wrap:normal;overflow-x:auto}input::placeholder,textarea::placeholder{color:var(--fg-mute)}label{font-size:var(--t-sm);color:var(--fg-dim)}.field{display:grid;gap:var(--s1);min-width:0}.check{display:inline-flex;align-items:center;gap:var(--s2);cursor:pointer;color:var(--fg)}.check input{width:auto;min-height:0;accent-color:var(--accent)}.kv{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr) auto;gap:var(--s2);margin-bottom:var(--s2);align-items:center}.kv.head{font-size:var(--t-xs);color:var(--fg-mute);text-transform:uppercase;letter-spacing:.06em}@media(max-width:560px){.kv{grid-template-columns:minmax(0,1fr) auto}.kv.head{display:none}}.badge{display:inline-flex;align-items:center;gap:5px;font:500 var(--t-xs) / 1 var(--sans);padding:4px 8px;border-radius:999px;background:var(--neutral-soft);color:var(--fg-dim);border:1px solid transparent;white-space:nowrap}.badge.ok{background:var(--ok-soft);color:var(--ok)}.badge.failed,.badge.fail{background:var(--fail-soft);color:var(--fail)}.badge.leaked,.badge.warn{background:var(--leak-soft);color:var(--leak)}.badge.running,.badge.busy,.badge.info{background:var(--accent-soft);color:var(--accent)}.badge.unknown{background:var(--neutral-soft);color:var(--neutral)}.badge.shared{background:var(--leak-soft);color:var(--leak)}.badge.isolated{background:var(--accent-soft);color:var(--accent)}.badge.off{opacity:.38}.badge .dot{width:6px;height:6px;border-radius:50%;background:currentColor}.badge.running .dot,.dot.pulse{animation:pulse 1.6s var(--ease) infinite}.banner{border:1px solid var(--line);border-left-width:3px;border-radius:var(--r2);padding:var(--s3) var(--s4);margin:var(--s3) 0;background:var(--panel-2);font-size:var(--t-sm)}.banner b{color:var(--fg)}.banner p{margin-top:var(--s2);color:var(--fg-dim)}.banner.ok{border-left-color:var(--ok);background:var(--ok-soft)}.banner.failed{border-left-color:var(--fail);background:var(--fail-soft)}.banner.leaked,.banner.warn{border-left-color:var(--leak);background:var(--leak-soft)}.banner.info{border-left-color:var(--accent);background:var(--accent-soft)}.banner.plain{border-left-color:var(--fg-mute)}.hint{font-size:var(--t-sm);color:var(--fg-mute);margin-top:var(--s3)}.hint code,.banner code{background:var(--sunk);padding:1px 5px;border-radius:4px}table{width:100%;border-collapse:collapse;font-size:var(--t-sm)}th{text-align:left;font:600 var(--t-xs) / 1 var(--sans);text-transform:uppercase;letter-spacing:.07em;color:var(--fg-mute);padding:0 var(--s3) var(--s2);border-bottom:1px solid var(--line)}td{padding:var(--s3);border-bottom:1px solid var(--line-soft);vertical-align:middle}tbody tr{transition:background var(--dur-fast) var(--ease)}tbody tr:hover{background:var(--panel-2)}td.name{font-family:var(--mono);font-size:var(--t-sm);word-break:break-all}@media(max-width:640px){table.cards,table.cards tbody,table.cards tr,table.cards td{display:block;width:100%}table.cards thead{display:none}table.cards tr{border:1px solid var(--line);border-radius:var(--r2);padding:var(--s2) var(--s3);margin-bottom:var(--s2);background:var(--panel-2)}table.cards td{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s3);border:0;padding:var(--s1) 0;text-align:right}table.cards td:before{content:attr(data-label);font:600 var(--t-xs) / 1.6 var(--sans);text-transform:uppercase;letter-spacing:.06em;color:var(--fg-mute);text-align:left;flex:none}table.cards td[colspan]{text-align:left}table.cards td[colspan]:before{content:""}}.kvlist{display:grid;gap:1px;background:var(--line-soft);border-radius:var(--r2);overflow:hidden}.kvlist>li{display:flex;gap:var(--s3);padding:var(--s3);background:var(--panel);align-items:baseline;flex-wrap:wrap}.kvlist>li>.k{flex:none;width:128px;color:var(--fg-dim);font-size:var(--t-sm)}.kvlist>li>.v{flex:1;min-width:0;word-break:break-word}@media(max-width:480px){.kvlist>li>.k{width:100%}}pre.raw,pre.log{font-family:var(--mono);font-size:var(--t-sm);line-height:1.6;background:var(--sunk);border:1px solid var(--line);border-radius:var(--r2);padding:var(--s3);margin:var(--s3) 0 0;overflow:auto;white-space:pre}pre.raw{color:var(--fail);max-height:420px}pre.log{max-height:460px;color:var(--fg-dim)}pre.log .t{color:var(--fg-mute)}pre.log .lv-error{color:var(--fail)}pre.log .lv-warn{color:var(--leak)}pre.log .lv-info{color:var(--fg)}pre.log mark{background:var(--leak-soft);color:var(--leak)}.masked{color:var(--fg-mute);letter-spacing:.12em}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(96px,1fr));gap:var(--s2)}.stat{background:var(--panel-2);border:1px solid var(--line-soft);border-radius:var(--r2);padding:var(--s3)}.stat .v{font:600 var(--t-2xl) / 1.1 var(--sans);letter-spacing:-.02em;font-variant-numeric:tabular-nums}.stat .k{font-size:var(--t-xs);color:var(--fg-mute);text-transform:uppercase;letter-spacing:.06em;margin-top:2px}.stat.unknown .v{color:var(--neutral)}.stat.busy .v{color:var(--accent)}.stat.leak .v{color:var(--leak)}.tabs{display:flex;gap:var(--s1);border-bottom:1px solid var(--line);margin-bottom:var(--s4);overflow-x:auto;scrollbar-width:none}.tabs::-webkit-scrollbar{display:none}.tabs a{padding:var(--s2) var(--s3);color:var(--fg-dim);font-size:var(--t-sm);font-weight:500;border-bottom:2px solid transparent;margin-bottom:-1px;white-space:nowrap;transition:color var(--dur-fast) var(--ease),border-color var(--dur-fast) var(--ease)}.tabs a:hover{color:var(--fg);text-decoration:none}.tabs a.router-link-active{color:var(--fg);border-bottom-color:var(--accent)}.steps{display:grid;gap:1px;background:var(--line-soft);border-radius:var(--r2);overflow:hidden}.step{display:grid;grid-template-columns:24px minmax(0,1fr);gap:var(--s3);padding:var(--s3);background:var(--panel);align-items:start}.step .mark{font:600 var(--t-lg) / 1.2 var(--mono);text-align:center}.step .who{font-family:var(--mono);word-break:break-all}.step .msg{font-size:var(--t-sm);color:var(--fg-dim);word-break:break-word}.s-ok{color:var(--ok)}.s-failed{color:var(--fail)}.s-leaked{color:var(--leak)}.s-unverifiable{color:var(--neutral)}.skel{background:var(--skel);background-size:200% 100%;border-radius:var(--r1);height:14px;animation:shimmer 1.3s linear infinite}.skel+.skel{margin-top:var(--s2)}.skel.tall{height:44px}.skel.w60{width:60%}.skel.w40{width:40%}@keyframes shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.35;transform:scale(.8)}}@keyframes indeterminate{0%{transform:scaleX(0)}50%{transform:scaleX(.7)}to{transform:scaleX(0);transform-origin:right}}@keyframes rise{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}@keyframes land{0%{transform:scale(.985);opacity:.4}60%{transform:scale(1.004)}to{transform:none;opacity:1}}.stagger>*{animation:rise var(--dur) var(--ease) backwards;animation-delay:calc(var(--i, 0) * var(--stagger-step))}.land{animation:land var(--dur-slow) var(--ease)}.view-enter-active,.view-leave-active{transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease)}.view-enter-from{opacity:0;transform:translateY(6px)}.view-leave-to{opacity:0;transform:translateY(-4px)}.toasts{position:fixed;right:var(--s4);bottom:var(--s4);z-index:50;display:grid;gap:var(--s2);width:min(360px,calc(100vw - var(--s5)))}@media(max-width:800px){.toasts{bottom:calc(84px + env(safe-area-inset-bottom))}}.toast{display:flex;gap:var(--s3);align-items:flex-start;background:var(--panel);border:1px solid var(--line);border-left-width:3px;border-radius:var(--r2);padding:var(--s3);box-shadow:var(--shadow);font-size:var(--t-sm)}.toast.ok{border-left-color:var(--ok)}.toast.error{border-left-color:var(--fail)}.toast.warn{border-left-color:var(--leak)}.toast.info{border-left-color:var(--accent)}.toast .x{margin-left:auto;background:none;border:0;min-height:0;padding:2px 6px;color:var(--fg-mute)}.toast-enter-active,.toast-leave-active{transition:opacity var(--dur) var(--ease),transform var(--dur) var(--ease)}.toast-enter-from,.toast-leave-to{opacity:0;transform:translate(12px)}.toast-move{transition:transform var(--dur) var(--ease)}.scrim{position:fixed;top:0;right:0;bottom:0;left:0;background:#0000008c;z-index:60;display:grid;place-items:center;padding:var(--s4)}.sheet{background:var(--panel);border:1px solid var(--line);border-radius:var(--r3);box-shadow:var(--shadow);padding:var(--s5);width:min(460px,100%);max-height:80vh;overflow:auto}.sheet dl{display:grid;grid-template-columns:auto minmax(0,1fr);gap:var(--s2) var(--s4);margin:var(--s4) 0 0;align-items:baseline}.sheet dt{text-align:right}.sheet dd{margin:0;color:var(--fg-dim);font-size:var(--t-sm)}kbd{font:500 var(--t-xs) / 1 var(--mono);background:var(--sunk);border:1px solid var(--line);border-bottom-width:2px;border-radius:5px;padding:4px 6px;color:var(--fg)}.fade-enter-active,.fade-leave-active{transition:opacity var(--dur) var(--ease)}.fade-enter-from,.fade-leave-to{opacity:0}
@@ -0,0 +1,2 @@
1
+ import{L as n,m as i,k as d,p as l,G as p,M as c}from"./index--8-Mcu3f.js";import{w as u,r as y}from"./useVars-VN9gYdR6.js";const e=n({id:"",detail:null,error:"",loading:!1,vars:[]}),f=i(()=>p(e.vars)),k=i(()=>c(e.id)),b=i(()=>{var a;return((a=e.detail)==null?void 0:a.kind)==="shared"}),h=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 d.get(`/api/deployments/${encodeURIComponent(a)}${f.value}`);if(e.id!==a)return;if(e.loading=!1,!s.ok){e.detail=null,e.error=l(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,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(){u(e.id,e.vars)}function x(){m(),t()}export{x as a,e as d,b as i,g as o,m as p,k as r,h as u,f as v};
2
+ //# sourceMappingURL=useDeployment-ByQUNe_X.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDeployment-ByQUNe_X.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 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,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"}
@@ -0,0 +1,2 @@
1
+ function a(t){return t?new Date(t).toLocaleString("sv"):"—"}function u(t,r){if(!t)return"—";const n=Math.max(0,Math.round(((r??Date.now())-t)/1e3)),o=n<60?`${n}s`:`${Math.floor(n/60)}m${String(n%60).padStart(2,"0")}s`;return r?o:`${o}…`}function e(t){if(!t)return"—";const r=Math.max(0,Math.round((Date.now()-t)/1e3));if(r<45)return"just now";const n=Math.round(r/60);if(n<60)return`${n}m ago`;const o=Math.round(n/60);return o<24?`${o}h ago`:`${Math.round(o/24)}d ago`}export{e as a,a as s,u as t};
2
+ //# sourceMappingURL=useFormat-CNPlQbIu.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useFormat-CNPlQbIu.js","sources":["../../src/composables/useFormat.ts"],"sourcesContent":["/**\n * Formatting shared across views. All times are the VIEWER's local time, with no zone marker,\n * because everything on the page — log lines, job stamps, deployment timestamps — comes from the\n * same clock and mixing a rendered zone into one of them is how two timestamps stop comparing.\n */\n\n/** ISO 8601 shaped and sortable: `2026-07-28 05:17:00`. `sv` is the locale that renders it. */\nexport function stamp(ms: number | undefined): string {\n return ms ? new Date(ms).toLocaleString('sv') : '—';\n}\n\n/** Just the clock, for a log line where the date is the same for every row. */\nexport function hhmmss(ms: number | undefined): string {\n return ms ? new Date(ms).toTimeString().slice(0, 8) : '--:--:--';\n}\n\n/** `12s` / `3m07s`. A running job gets an ellipsis so a stale number cannot read as a final one. */\nexport function took(startedAt: number | undefined, endedAt?: number): string {\n if (!startedAt) return '—';\n const s = Math.max(0, Math.round(((endedAt ?? Date.now()) - startedAt) / 1000));\n const text = s < 60 ? `${s}s` : `${Math.floor(s / 60)}m${String(s % 60).padStart(2, '0')}s`;\n return endedAt ? text : `${text}…`;\n}\n\n/** `2 minutes ago`, for an activity feed where the exact second is noise. */\nexport function ago(ms: number | undefined): string {\n if (!ms) return '—';\n const s = Math.max(0, Math.round((Date.now() - ms) / 1000));\n if (s < 45) return 'just now';\n const m = Math.round(s / 60);\n if (m < 60) return `${m}m ago`;\n const h = Math.round(m / 60);\n if (h < 24) return `${h}h ago`;\n return `${Math.round(h / 24)}d ago`;\n}\n"],"names":["stamp","ms","took","startedAt","endedAt","s","text","ago","m","h"],"mappings":"AAOO,SAASA,EAAMC,EAAgC,CACpD,OAAOA,EAAK,IAAI,KAAKA,CAAE,EAAE,eAAe,IAAI,EAAI,GAClD,CAQO,SAASC,EAAKC,EAA+BC,EAA0B,CAC5E,GAAI,CAACD,EAAW,MAAO,IACvB,MAAME,EAAI,KAAK,IAAI,EAAG,KAAK,QAAQD,GAAW,KAAK,IAAA,GAASD,GAAa,GAAI,CAAC,EACxEG,EAAOD,EAAI,GAAK,GAAGA,CAAC,IAAM,GAAG,KAAK,MAAMA,EAAI,EAAE,CAAC,IAAI,OAAOA,EAAI,EAAE,EAAE,SAAS,EAAG,GAAG,CAAC,IACxF,OAAOD,EAAUE,EAAO,GAAGA,CAAI,GACjC,CAGO,SAASC,EAAIN,EAAgC,CAClD,GAAI,CAACA,EAAI,MAAO,IAChB,MAAMI,EAAI,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,IAAA,EAAQJ,GAAM,GAAI,CAAC,EAC1D,GAAII,EAAI,GAAI,MAAO,WACnB,MAAMG,EAAI,KAAK,MAAMH,EAAI,EAAE,EAC3B,GAAIG,EAAI,GAAI,MAAO,GAAGA,CAAC,QACvB,MAAMC,EAAI,KAAK,MAAMD,EAAI,EAAE,EAC3B,OAAIC,EAAI,GAAW,GAAGA,CAAC,QAChB,GAAG,KAAK,MAAMA,EAAI,EAAE,CAAC,OAC9B"}
@@ -0,0 +1,2 @@
1
+ const i="pstack.ui.vars.";function c(n){try{const t=JSON.parse(localStorage.getItem(i+n)??"[]");if(!Array.isArray(t))return[{k:"",v:""}];const r=t.filter(a=>!!a&&typeof a.k=="string").map(a=>({k:a.k,v:String(a.v??"")}));return r.length?r:[{k:"",v:""}]}catch{return[{k:"",v:""}]}}function o(n,t){if(n)try{localStorage.setItem(i+n,JSON.stringify(t.filter(r=>r.k)))}catch{}}function s(n){const t=Object.entries(n??{}).map(([r,a])=>({k:r,v:a}));return t.length?t:[{k:"",v:""}]}function e(n){const t={};for(const r of n)r.k&&(t[r.k]=r.v);return t}function f(n,t){return t?n.filter(r=>r.k&&t[r.k]!==void 0&&t[r.k]!==r.v).map(r=>r.k):[]}export{e as a,f as c,s as p,c as r,o as w};
2
+ //# sourceMappingURL=useVars-VN9gYdR6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useVars-VN9gYdR6.js","sources":["../../src/composables/useVars.ts"],"sourcesContent":["/**\n * Per-deployment variables, kept in this browser.\n *\n * THE SAFETY RULE ON THIS PAGE. A spec interpolates `${VAR}` exactly once, at resolve time, and\n * every route that resolves a deployment layers the request's query parameters over the server's\n * own environment. So `up` sent with `PR=7` and a later `down` sent without it resolve to two\n * DIFFERENT stacks, and the teardown quietly misses every container the deploy created — the exact\n * leak this project exists to prevent.\n *\n * Newer servers also STORE variables with the deployment (`vars` on the registry meta), which\n * makes the pair self-describing and is strictly better. This module does not replace that:\n * - a request variable still OVERRIDES a stored one, so a wrong value typed here still retargets\n * the stack — the warning stays true and stays visible;\n * - a deployment submitted to an older server, or submitted without `vars`, has nothing stored,\n * and then these pairs are the only thing keeping `up` and `down` matched.\n *\n * Keyed by deployment id. Corrupt or unavailable storage degrades to one empty row, never to a\n * thrown error — losing a convenience must not cost the page.\n */\n\nimport type { VarPair } from '../api/types';\n\nconst PREFIX = 'pstack.ui.vars.';\n\nexport function readVars(id: string): VarPair[] {\n try {\n const raw: unknown = JSON.parse(localStorage.getItem(PREFIX + id) ?? '[]');\n if (!Array.isArray(raw)) return [{ k: '', v: '' }];\n const pairs = raw\n .filter((e): e is { k: string; v?: unknown } => !!e && typeof (e as VarPair).k === 'string')\n .map((e) => ({ k: e.k, v: String(e.v ?? '') }));\n return pairs.length ? pairs : [{ k: '', v: '' }];\n } catch {\n return [{ k: '', v: '' }];\n }\n}\n\nexport function writeVars(id: string, pairs: VarPair[]): void {\n if (!id) return;\n try {\n localStorage.setItem(PREFIX + id, JSON.stringify(pairs.filter((e) => e.k)));\n } catch {\n /* private mode / quota — the pairs still work for this page load */\n }\n}\n\n/** `{ PR: '7' }` → editor rows. Used to seed the editor from what the server has stored. */\nexport function pairsFromRecord(rec: Record<string, string> | undefined): VarPair[] {\n const pairs = Object.entries(rec ?? {}).map(([k, v]) => ({ k, v }));\n return pairs.length ? pairs : [{ k: '', v: '' }];\n}\n\nexport function recordFromPairs(pairs: VarPair[]): Record<string, string> {\n const out: Record<string, string> = {};\n for (const e of pairs) if (e.k) out[e.k] = e.v;\n return out;\n}\n\n/**\n * Does the request-time set disagree with what the server stored? That is the dangerous shape:\n * both resolve, so nothing errors — they just resolve to different stacks.\n */\nexport function conflictingVars(\n pairs: VarPair[],\n stored: Record<string, string> | undefined,\n): string[] {\n if (!stored) return [];\n return pairs.filter((e) => e.k && stored[e.k] !== undefined && stored[e.k] !== e.v).map((e) => e.k);\n}\n"],"names":["PREFIX","readVars","id","raw","pairs","e","writeVars","pairsFromRecord","rec","k","v","recordFromPairs","out","conflictingVars","stored"],"mappings":"AAsBA,MAAMA,EAAS,kBAER,SAASC,EAASC,EAAuB,CAC9C,GAAI,CACF,MAAMC,EAAe,KAAK,MAAM,aAAa,QAAQH,EAASE,CAAE,GAAK,IAAI,EACzE,GAAI,CAAC,MAAM,QAAQC,CAAG,EAAG,MAAO,CAAC,CAAE,EAAG,GAAI,EAAG,GAAI,EACjD,MAAMC,EAAQD,EACX,OAAQE,GAAuC,CAAC,CAACA,GAAK,OAAQA,EAAc,GAAM,QAAQ,EAC1F,IAAKA,IAAO,CAAE,EAAGA,EAAE,EAAG,EAAG,OAAOA,EAAE,GAAK,EAAE,CAAA,EAAI,EAChD,OAAOD,EAAM,OAASA,EAAQ,CAAC,CAAE,EAAG,GAAI,EAAG,GAAI,CACjD,MAAQ,CACN,MAAO,CAAC,CAAE,EAAG,GAAI,EAAG,GAAI,CAC1B,CACF,CAEO,SAASE,EAAUJ,EAAYE,EAAwB,CAC5D,GAAKF,EACL,GAAI,CACF,aAAa,QAAQF,EAASE,EAAI,KAAK,UAAUE,EAAM,OAAQC,GAAMA,EAAE,CAAC,CAAC,CAAC,CAC5E,MAAQ,CAER,CACF,CAGO,SAASE,EAAgBC,EAAoD,CAClF,MAAMJ,EAAQ,OAAO,QAAQI,GAAO,CAAA,CAAE,EAAE,IAAI,CAAC,CAACC,EAAGC,CAAC,KAAO,CAAE,EAAAD,EAAG,EAAAC,GAAI,EAClE,OAAON,EAAM,OAASA,EAAQ,CAAC,CAAE,EAAG,GAAI,EAAG,GAAI,CACjD,CAEO,SAASO,EAAgBP,EAA0C,CACxE,MAAMQ,EAA8B,CAAA,EACpC,UAAWP,KAAKD,EAAWC,EAAE,IAAGO,EAAIP,EAAE,CAAC,EAAIA,EAAE,GAC7C,OAAOO,CACT,CAMO,SAASC,EACdT,EACAU,EACU,CACV,OAAKA,EACEV,EAAM,OAAQC,GAAMA,EAAE,GAAKS,EAAOT,EAAE,CAAC,IAAM,QAAaS,EAAOT,EAAE,CAAC,IAAMA,EAAE,CAAC,EAAE,IAAKA,GAAMA,EAAE,CAAC,EAD9E,CAAA,CAEtB"}
@@ -0,0 +1,33 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <!-- An operations console for one host. Nothing here should ever be indexed. -->
7
+ <meta name="robots" content="noindex, nofollow" />
8
+ <title>pstack</title>
9
+ <script>
10
+ /**
11
+ * Theme, applied BEFORE first paint.
12
+ *
13
+ * The stylesheet defaults to the OS preference; a manual choice is stamped on <html> as
14
+ * data-theme. Doing it here rather than in the app avoids the flash of the wrong theme
15
+ * while Vue boots. Same for the motion override: a page that animates in before the
16
+ * "reduce motion" setting loads has already broken the promise.
17
+ */
18
+ try {
19
+ var t = localStorage.getItem('pstack.ui.theme');
20
+ if (t === 'dark' || t === 'light') document.documentElement.dataset.theme = t;
21
+ var m = localStorage.getItem('pstack.ui.motion');
22
+ if (m === 'none' || m === 'full') document.documentElement.dataset.motion = m;
23
+ } catch (e) {
24
+ /* private mode: fall through to the OS preference */
25
+ }
26
+ </script>
27
+ <script type="module" crossorigin src="/assets/index--8-Mcu3f.js"></script>
28
+ <link rel="stylesheet" crossorigin href="/assets/index-C_dmDOA0.css">
29
+ </head>
30
+ <body>
31
+ <div id="app"></div>
32
+ </body>
33
+ </html>
package/nginx.conf ADDED
@@ -0,0 +1,52 @@
1
+ # nginx for the advanced UI container.
2
+ #
3
+ # Two jobs, and the second is the reason this file exists at all.
4
+ server {
5
+ listen 80;
6
+ server_name _;
7
+
8
+ root /usr/share/nginx/html;
9
+ index index.html;
10
+
11
+ # Never cache the entry document. Vite fingerprints every asset, so a stale index.html is the one
12
+ # way a browser can end up asking for a chunk that no longer exists after an upgrade.
13
+ location = /index.html {
14
+ add_header Cache-Control "no-store";
15
+ try_files $uri =404;
16
+ }
17
+
18
+ # Fingerprinted assets are immutable by construction — a change produces a new filename.
19
+ location /assets/ {
20
+ add_header Cache-Control "public, max-age=31536000, immutable";
21
+ try_files $uri =404;
22
+ }
23
+
24
+ # ── The proxy — why this container is not just a static bucket ─────────────────────────────
25
+ # The SPA calls the API with RELATIVE paths (`/api/…`), so proxying here keeps the browser
26
+ # same-origin: no CORS preflight to configure, no cross-origin cookie rules, and nothing to change
27
+ # if the hostnames move. `pstack` is the control-stack service name, resolved on the shared
28
+ # ingress network.
29
+ #
30
+ # `proxy_buffering off` is load-bearing for the job log: the API streams Server-Sent Events, and
31
+ # with buffering on nginx would hold the response until a buffer filled — the log would appear to
32
+ # freeze and then arrive in bursts, which reads exactly like a hung job.
33
+ location /api/ {
34
+ proxy_pass http://pstack:7878;
35
+ proxy_http_version 1.1;
36
+ proxy_set_header Host $host;
37
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
38
+ proxy_set_header X-Forwarded-Proto $scheme;
39
+ proxy_set_header Connection "";
40
+ proxy_buffering off;
41
+ proxy_cache off;
42
+ # A job can outlive any sane request timeout; the SSE stream must not be cut mid-run.
43
+ proxy_read_timeout 1h;
44
+ }
45
+
46
+ # SPA fallback, LAST so it never shadows a real file or the proxy above. Without it every deep
47
+ # link (/deployments/pr-7, /jobs/xyz) 404s on reload — the router only exists once the app has
48
+ # loaded, and it cannot load if the server refuses the URL.
49
+ location / {
50
+ try_files $uri $uri/ /index.html;
51
+ }
52
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@samyx/preview-stacks-ui",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "description": "The advanced web UI for preview-stacks — an opt-in SPA that talks to the pstack HTTP API.",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/samishal1998/preview-stacks.git",
10
+ "directory": "apps/ui"
11
+ },
12
+ "homepage": "https://github.com/samishal1998/preview-stacks#readme",
13
+ "scripts": {
14
+ "dev": "vite",
15
+ "build": "vue-tsc --build && vite build",
16
+ "typecheck": "vue-tsc --build",
17
+ "preview": "vite preview",
18
+ "prepublishOnly": "bun run build"
19
+ },
20
+ "dependencies": {
21
+ "vue": "^3.5.13",
22
+ "vue-router": "^4.5.0"
23
+ },
24
+ "license": "MIT",
25
+ "author": "sami.mishal",
26
+ "files": [
27
+ "dist",
28
+ "nginx.conf",
29
+ "README.md",
30
+ "LICENSE"
31
+ ]
32
+ }