From cab813981500aedec55024dcc7a758f2d1a0433e Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 09:18:55 +0000 Subject: [PATCH 01/26] Update --- .env.example | 4 ++ Dockerfile | 23 ++++++++++++ backend/public/assets/index-0842cd3a.css | 1 + backend/public/assets/index-a1f09468.js | 48 ++++++++++++++++++++++++ backend/public/index.html | 15 ++++++++ docker-compose.dev.yml | 11 ++++++ scripts/start-dev.sh | 13 ++++++- scripts/start-dev.sh.bak | 24 ++++++++++++ 8 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 backend/public/assets/index-0842cd3a.css create mode 100644 backend/public/assets/index-a1f09468.js create mode 100644 backend/public/index.html create mode 100644 docker-compose.dev.yml create mode 100755 scripts/start-dev.sh.bak diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1c11102 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +PORT=4000 +NODE_ENV=production +PORTAINER_URL=https://your-portainer.example.com +PORTAINER_API_KEY=your_api_key_here diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9ba01f2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +# Stage 1: Frontend build +FROM node:20-alpine AS frontend-build +WORKDIR /app/frontend +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +# Stage 2: Backend + static frontend +FROM node:20-alpine AS runtime +WORKDIR /app/backend +COPY backend/package.json backend/package-lock.json ./ +RUN npm ci --only=production +COPY backend/ ./ +COPY --from=frontend-build /app/frontend/build ./public + +ENV NODE_ENV=production +EXPOSE 4000 # Port wird über .env gesetzt + +RUN chown -R node:node /app/backend +USER node + +CMD ["node", "index.js"] diff --git a/backend/public/assets/index-0842cd3a.css b/backend/public/assets/index-0842cd3a.css new file mode 100644 index 0000000..d468b6f --- /dev/null +++ b/backend/public/assets/index-0842cd3a.css @@ -0,0 +1 @@ +*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.mt-1{margin-top:.25rem}.flex{display:flex}.grid{display:grid}.h-12{height:3rem}.min-h-screen{min-height:100vh}.w-12{width:3rem}.cursor-not-allowed{cursor:not-allowed}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-6{gap:1.5rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{font-family:Inter,sans-serif;color:#fff}button{outline:none;border:none}button:disabled{opacity:.8}.hover\:bg-blue-600:hover{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}} diff --git a/backend/public/assets/index-a1f09468.js b/backend/public/assets/index-a1f09468.js new file mode 100644 index 0000000..9a96e4f --- /dev/null +++ b/backend/public/assets/index-a1f09468.js @@ -0,0 +1,48 @@ +(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const i of document.querySelectorAll('link[rel="modulepreload"]'))r(i);new MutationObserver(i=>{for(const o of i)if(o.type==="childList")for(const s of o.addedNodes)s.tagName==="LINK"&&s.rel==="modulepreload"&&r(s)}).observe(document,{childList:!0,subtree:!0});function n(i){const o={};return i.integrity&&(o.integrity=i.integrity),i.referrerPolicy&&(o.referrerPolicy=i.referrerPolicy),i.crossOrigin==="use-credentials"?o.credentials="include":i.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(i){if(i.ep)return;i.ep=!0;const o=n(i);fetch(i.href,o)}})();function Pd(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Da={exports:{}},$i={},za={exports:{}},A={};/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var vr=Symbol.for("react.element"),Ld=Symbol.for("react.portal"),Ad=Symbol.for("react.fragment"),Dd=Symbol.for("react.strict_mode"),zd=Symbol.for("react.profiler"),Fd=Symbol.for("react.provider"),Bd=Symbol.for("react.context"),Ud=Symbol.for("react.forward_ref"),Id=Symbol.for("react.suspense"),Md=Symbol.for("react.memo"),jd=Symbol.for("react.lazy"),su=Symbol.iterator;function $d(e){return e===null||typeof e!="object"?null:(e=su&&e[su]||e["@@iterator"],typeof e=="function"?e:null)}var Fa={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Ba=Object.assign,Ua={};function kn(e,t,n){this.props=e,this.context=t,this.refs=Ua,this.updater=n||Fa}kn.prototype.isReactComponent={};kn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};kn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Ia(){}Ia.prototype=kn.prototype;function el(e,t,n){this.props=e,this.context=t,this.refs=Ua,this.updater=n||Fa}var tl=el.prototype=new Ia;tl.constructor=el;Ba(tl,kn.prototype);tl.isPureReactComponent=!0;var lu=Array.isArray,Ma=Object.prototype.hasOwnProperty,nl={current:null},ja={key:!0,ref:!0,__self:!0,__source:!0};function $a(e,t,n){var r,i={},o=null,s=null;if(t!=null)for(r in t.ref!==void 0&&(s=t.ref),t.key!==void 0&&(o=""+t.key),t)Ma.call(t,r)&&!ja.hasOwnProperty(r)&&(i[r]=t[r]);var l=arguments.length-2;if(l===1)i.children=n;else if(1>>1,ee=T[Q];if(0>>1;Qi(mo,L))Lti(Tr,mo)?(T[Q]=Tr,T[Lt]=L,Q=Lt):(T[Q]=mo,T[Pt]=L,Q=Pt);else if(Lti(Tr,L))T[Q]=Tr,T[Lt]=L,Q=Lt;else break e}}return P}function i(T,P){var L=T.sortIndex-P.sortIndex;return L!==0?L:T.id-P.id}if(typeof performance=="object"&&typeof performance.now=="function"){var o=performance;e.unstable_now=function(){return o.now()}}else{var s=Date,l=s.now();e.unstable_now=function(){return s.now()-l}}var u=[],a=[],c=1,p=null,m=3,S=!1,g=!1,v=!1,k=typeof setTimeout=="function"?setTimeout:null,d=typeof clearTimeout=="function"?clearTimeout:null,f=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function h(T){for(var P=n(a);P!==null;){if(P.callback===null)r(a);else if(P.startTime<=T)r(a),P.sortIndex=P.expirationTime,t(u,P);else break;P=n(a)}}function w(T){if(v=!1,h(T),!g)if(n(u)!==null)g=!0,po(_);else{var P=n(a);P!==null&&ho(w,P.startTime-T)}}function _(T,P){g=!1,v&&(v=!1,d(N),N=-1),S=!0;var L=m;try{for(h(P),p=n(u);p!==null&&(!(p.expirationTime>P)||T&&!ze());){var Q=p.callback;if(typeof Q=="function"){p.callback=null,m=p.priorityLevel;var ee=Q(p.expirationTime<=P);P=e.unstable_now(),typeof ee=="function"?p.callback=ee:p===n(u)&&r(u),h(P)}else r(u);p=n(u)}if(p!==null)var Rr=!0;else{var Pt=n(a);Pt!==null&&ho(w,Pt.startTime-P),Rr=!1}return Rr}finally{p=null,m=L,S=!1}}var x=!1,R=null,N=-1,j=5,D=-1;function ze(){return!(e.unstable_now()-DT||125Q?(T.sortIndex=L,t(a,T),n(u)===null&&T===n(a)&&(v?(d(N),N=-1):v=!0,ho(w,L-Q))):(T.sortIndex=ee,t(u,T),g||S||(g=!0,po(_))),T},e.unstable_shouldYield=ze,e.unstable_wrapCallback=function(T){var P=m;return function(){var L=m;m=P;try{return T.apply(this,arguments)}finally{m=L}}}})(Qa);qa.exports=Qa;var bd=qa.exports;/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */var ep=be,xe=bd;function E(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Jo=Object.prototype.hasOwnProperty,tp=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,au={},cu={};function np(e){return Jo.call(cu,e)?!0:Jo.call(au,e)?!1:tp.test(e)?cu[e]=!0:(au[e]=!0,!1)}function rp(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function ip(e,t,n,r){if(t===null||typeof t>"u"||rp(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function he(e,t,n,r,i,o,s){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=i,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=o,this.removeEmptyString=s}var oe={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){oe[e]=new he(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];oe[t]=new he(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){oe[e]=new he(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){oe[e]=new he(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){oe[e]=new he(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){oe[e]=new he(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){oe[e]=new he(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){oe[e]=new he(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){oe[e]=new he(e,5,!1,e.toLowerCase(),null,!1,!1)});var il=/[\-:]([a-z])/g;function ol(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(il,ol);oe[t]=new he(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(il,ol);oe[t]=new he(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(il,ol);oe[t]=new he(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!1,!1)});oe.xlinkHref=new he("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){oe[e]=new he(e,1,!1,e.toLowerCase(),null,!0,!0)});function sl(e,t,n,r){var i=oe.hasOwnProperty(t)?oe[t]:null;(i!==null?i.type!==0:r||!(2l||i[s]!==o[l]){var u=` +`+i[s].replace(" at new "," at ");return e.displayName&&u.includes("")&&(u=u.replace("",e.displayName)),u}while(1<=s&&0<=l);break}}}finally{vo=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?jn(e):""}function op(e){switch(e.tag){case 5:return jn(e.type);case 16:return jn("Lazy");case 13:return jn("Suspense");case 19:return jn("SuspenseList");case 0:case 2:case 15:return e=wo(e.type,!1),e;case 11:return e=wo(e.type.render,!1),e;case 1:return e=wo(e.type,!0),e;default:return""}}function es(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Jt:return"Fragment";case Yt:return"Portal";case Go:return"Profiler";case ll:return"StrictMode";case Zo:return"Suspense";case bo:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ya:return(e.displayName||"Context")+".Consumer";case Xa:return(e._context.displayName||"Context")+".Provider";case ul:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case al:return t=e.displayName||null,t!==null?t:es(e.type)||"Memo";case ct:t=e._payload,e=e._init;try{return es(e(t))}catch{}}return null}function sp(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return es(t);case 8:return t===ll?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Ct(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Ga(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function lp(e){var t=Ga(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var i=n.get,o=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return i.call(this)},set:function(s){r=""+s,o.call(this,s)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(s){r=""+s},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function Pr(e){e._valueTracker||(e._valueTracker=lp(e))}function Za(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Ga(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function hi(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function ts(e,t){var n=t.checked;return W({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function du(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Ct(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ba(e,t){t=t.checked,t!=null&&sl(e,"checked",t,!1)}function ns(e,t){ba(e,t);var n=Ct(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?rs(e,t.type,n):t.hasOwnProperty("defaultValue")&&rs(e,t.type,Ct(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function pu(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function rs(e,t,n){(t!=="number"||hi(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var $n=Array.isArray;function un(e,t,n,r){if(e=e.options,t){t={};for(var i=0;i"+t.valueOf().toString()+"",t=Lr.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function tr(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}var qn={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},up=["Webkit","ms","Moz","O"];Object.keys(qn).forEach(function(e){up.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),qn[t]=qn[e]})});function rc(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||qn.hasOwnProperty(e)&&qn[e]?(""+t).trim():t+"px"}function ic(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var r=n.indexOf("--")===0,i=rc(n,t[n],r);n==="float"&&(n="cssFloat"),r?e.setProperty(n,i):e[n]=i}}var ap=W({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ss(e,t){if(t){if(ap[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(E(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(E(60));if(typeof t.dangerouslySetInnerHTML!="object"||!("__html"in t.dangerouslySetInnerHTML))throw Error(E(61))}if(t.style!=null&&typeof t.style!="object")throw Error(E(62))}}function ls(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var us=null;function cl(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}var as=null,an=null,cn=null;function yu(e){if(e=Er(e)){if(typeof as!="function")throw Error(E(280));var t=e.stateNode;t&&(t=Qi(t),as(e.stateNode,e.type,t))}}function oc(e){an?cn?cn.push(e):cn=[e]:an=e}function sc(){if(an){var e=an,t=cn;if(cn=an=null,yu(e),t)for(e=0;e>>=0,e===0?32:31-(Sp(e)/Ep|0)|0}var Ar=64,Dr=4194304;function Vn(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return e&4194240;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return e&130023424;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function vi(e,t){var n=e.pendingLanes;if(n===0)return 0;var r=0,i=e.suspendedLanes,o=e.pingedLanes,s=n&268435455;if(s!==0){var l=s&~i;l!==0?r=Vn(l):(o&=s,o!==0&&(r=Vn(o)))}else s=n&~i,s!==0?r=Vn(s):o!==0&&(r=Vn(o));if(r===0)return 0;if(t!==0&&t!==r&&!(t&i)&&(i=r&-r,o=t&-t,i>=o||i===16&&(o&4194240)!==0))return t;if(r&4&&(r|=n&16),t=e.entangledLanes,t!==0)for(e=e.entanglements,t&=r;0n;n++)t.push(e);return t}function wr(e,t,n){e.pendingLanes|=t,t!==536870912&&(e.suspendedLanes=0,e.pingedLanes=0),e=e.eventTimes,t=31-je(t),e[t]=n}function Cp(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0=Kn),Cu=String.fromCharCode(32),Ru=!1;function Rc(e,t){switch(e){case"keyup":return bp.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function Tc(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}var Gt=!1;function th(e,t){switch(e){case"compositionend":return Tc(t);case"keypress":return t.which!==32?null:(Ru=!0,Cu);case"textInput":return e=t.data,e===Cu&&Ru?null:e;default:return null}}function nh(e,t){if(Gt)return e==="compositionend"||!vl&&Rc(e,t)?(e=xc(),Jr=ml=ht=null,Gt=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Pu(n)}}function Lc(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Lc(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ac(){for(var e=window,t=hi();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch{n=!1}if(n)e=t.contentWindow;else break;t=hi(e.document)}return t}function wl(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}function fh(e){var t=Ac(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&Lc(n.ownerDocument.documentElement,n)){if(r!==null&&wl(n)){if(t=r.start,e=r.end,e===void 0&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if(e=(t=n.ownerDocument||document)&&t.defaultView||window,e.getSelection){e=e.getSelection();var i=n.textContent.length,o=Math.min(r.start,i);r=r.end===void 0?o:Math.min(r.end,i),!e.extend&&o>r&&(i=r,r=o,o=i),i=Lu(n,o);var s=Lu(n,r);i&&s&&(e.rangeCount!==1||e.anchorNode!==i.node||e.anchorOffset!==i.offset||e.focusNode!==s.node||e.focusOffset!==s.offset)&&(t=t.createRange(),t.setStart(i.node,i.offset),e.removeAllRanges(),o>r?(e.addRange(t),e.extend(s.node,s.offset)):(t.setEnd(s.node,s.offset),e.addRange(t)))}}for(t=[],e=n;e=e.parentNode;)e.nodeType===1&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for(typeof n.focus=="function"&&n.focus(),n=0;n=document.documentMode,Zt=null,ms=null,Yn=null,ys=!1;function Au(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;ys||Zt==null||Zt!==hi(r)||(r=Zt,"selectionStart"in r&&wl(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Yn&&lr(Yn,r)||(Yn=r,r=Ei(ms,"onSelect"),0tn||(e.current=ks[tn],ks[tn]=null,tn--)}function U(e,t){tn++,ks[tn]=e.current,e.current=t}var Rt={},ce=Nt(Rt),ge=Nt(!1),jt=Rt;function yn(e,t){var n=e.type.contextTypes;if(!n)return Rt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var i={},o;for(o in n)i[o]=t[o];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=i),i}function ve(e){return e=e.childContextTypes,e!=null}function _i(){M(ge),M(ce)}function Mu(e,t,n){if(ce.current!==Rt)throw Error(E(168));U(ce,t),U(ge,n)}function $c(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var i in r)if(!(i in t))throw Error(E(108,sp(e)||"Unknown",i));return W({},n,r)}function xi(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Rt,jt=ce.current,U(ce,e),U(ge,ge.current),!0}function ju(e,t,n){var r=e.stateNode;if(!r)throw Error(E(169));n?(e=$c(e,t,jt),r.__reactInternalMemoizedMergedChildContext=e,M(ge),M(ce),U(ce,e)):M(ge),U(ge,n)}var et=null,Ki=!1,Do=!1;function Vc(e){et===null?et=[e]:et.push(e)}function _h(e){Ki=!0,Vc(e)}function Ot(){if(!Do&&et!==null){Do=!0;var e=0,t=B;try{var n=et;for(B=1;e>=s,i-=s,tt=1<<32-je(t)+i|n<N?(j=R,R=null):j=R.sibling;var D=m(d,R,h[N],w);if(D===null){R===null&&(R=j);break}e&&R&&D.alternate===null&&t(d,R),f=o(D,f,N),x===null?_=D:x.sibling=D,x=D,R=j}if(N===h.length)return n(d,R),$&&At(d,N),_;if(R===null){for(;NN?(j=R,R=null):j=R.sibling;var ze=m(d,R,D.value,w);if(ze===null){R===null&&(R=j);break}e&&R&&ze.alternate===null&&t(d,R),f=o(ze,f,N),x===null?_=ze:x.sibling=ze,x=ze,R=j}if(D.done)return n(d,R),$&&At(d,N),_;if(R===null){for(;!D.done;N++,D=h.next())D=p(d,D.value,w),D!==null&&(f=o(D,f,N),x===null?_=D:x.sibling=D,x=D);return $&&At(d,N),_}for(R=r(d,R);!D.done;N++,D=h.next())D=S(R,d,N,D.value,w),D!==null&&(e&&D.alternate!==null&&R.delete(D.key===null?N:D.key),f=o(D,f,N),x===null?_=D:x.sibling=D,x=D);return e&&R.forEach(function(Nn){return t(d,Nn)}),$&&At(d,N),_}function k(d,f,h,w){if(typeof h=="object"&&h!==null&&h.type===Jt&&h.key===null&&(h=h.props.children),typeof h=="object"&&h!==null){switch(h.$$typeof){case Or:e:{for(var _=h.key,x=f;x!==null;){if(x.key===_){if(_=h.type,_===Jt){if(x.tag===7){n(d,x.sibling),f=i(x,h.props.children),f.return=d,d=f;break e}}else if(x.elementType===_||typeof _=="object"&&_!==null&&_.$$typeof===ct&&Hu(_)===x.type){n(d,x.sibling),f=i(x,h.props),f.ref=Fn(d,x,h),f.return=d,d=f;break e}n(d,x);break}else t(d,x);x=x.sibling}h.type===Jt?(f=Mt(h.props.children,d.mode,w,h.key),f.return=d,d=f):(w=ii(h.type,h.key,h.props,null,d.mode,w),w.ref=Fn(d,f,h),w.return=d,d=w)}return s(d);case Yt:e:{for(x=h.key;f!==null;){if(f.key===x)if(f.tag===4&&f.stateNode.containerInfo===h.containerInfo&&f.stateNode.implementation===h.implementation){n(d,f.sibling),f=i(f,h.children||[]),f.return=d,d=f;break e}else{n(d,f);break}else t(d,f);f=f.sibling}f=$o(h,d.mode,w),f.return=d,d=f}return s(d);case ct:return x=h._init,k(d,f,x(h._payload),w)}if($n(h))return g(d,f,h,w);if(Pn(h))return v(d,f,h,w);jr(d,h)}return typeof h=="string"&&h!==""||typeof h=="number"?(h=""+h,f!==null&&f.tag===6?(n(d,f.sibling),f=i(f,h),f.return=d,d=f):(n(d,f),f=jo(h,d.mode,w),f.return=d,d=f),s(d)):n(d,f)}return k}var vn=Qc(!0),Kc=Qc(!1),Ti=Nt(null),Ni=null,on=null,_l=null;function xl(){_l=on=Ni=null}function Cl(e){var t=Ti.current;M(Ti),e._currentValue=t}function Cs(e,t,n){for(;e!==null;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,r!==null&&(r.childLanes|=t)):r!==null&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function dn(e,t){Ni=e,_l=on=null,e=e.dependencies,e!==null&&e.firstContext!==null&&(e.lanes&t&&(ye=!0),e.firstContext=null)}function Ae(e){var t=e._currentValue;if(_l!==e)if(e={context:e,memoizedValue:t,next:null},on===null){if(Ni===null)throw Error(E(308));on=e,Ni.dependencies={lanes:0,firstContext:e}}else on=on.next=e;return t}var Ft=null;function Rl(e){Ft===null?Ft=[e]:Ft.push(e)}function Xc(e,t,n,r){var i=t.interleaved;return i===null?(n.next=n,Rl(t)):(n.next=i.next,i.next=n),t.interleaved=n,st(e,r)}function st(e,t){e.lanes|=t;var n=e.alternate;for(n!==null&&(n.lanes|=t),n=e,e=e.return;e!==null;)e.childLanes|=t,n=e.alternate,n!==null&&(n.childLanes|=t),n=e,e=e.return;return n.tag===3?n.stateNode:null}var ft=!1;function Tl(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Yc(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function rt(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function St(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,F&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,st(e,n)}return i=r.interleaved,i===null?(t.next=t,Rl(r)):(t.next=i.next,i.next=t),r.interleaved=t,st(e,n)}function Zr(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,(n&4194240)!==0)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,dl(e,n)}}function Wu(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,o=null;if(n=n.firstBaseUpdate,n!==null){do{var s={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};o===null?i=o=s:o=o.next=s,n=n.next}while(n!==null);o===null?i=o=t:o=o.next=t}else i=o=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:o,shared:r.shared,effects:r.effects},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Oi(e,t,n,r){var i=e.updateQueue;ft=!1;var o=i.firstBaseUpdate,s=i.lastBaseUpdate,l=i.shared.pending;if(l!==null){i.shared.pending=null;var u=l,a=u.next;u.next=null,s===null?o=a:s.next=a,s=u;var c=e.alternate;c!==null&&(c=c.updateQueue,l=c.lastBaseUpdate,l!==s&&(l===null?c.firstBaseUpdate=a:l.next=a,c.lastBaseUpdate=u))}if(o!==null){var p=i.baseState;s=0,c=a=u=null,l=o;do{var m=l.lane,S=l.eventTime;if((r&m)===m){c!==null&&(c=c.next={eventTime:S,lane:0,tag:l.tag,payload:l.payload,callback:l.callback,next:null});e:{var g=e,v=l;switch(m=t,S=n,v.tag){case 1:if(g=v.payload,typeof g=="function"){p=g.call(S,p,m);break e}p=g;break e;case 3:g.flags=g.flags&-65537|128;case 0:if(g=v.payload,m=typeof g=="function"?g.call(S,p,m):g,m==null)break e;p=W({},p,m);break e;case 2:ft=!0}}l.callback!==null&&l.lane!==0&&(e.flags|=64,m=i.effects,m===null?i.effects=[l]:m.push(l))}else S={eventTime:S,lane:m,tag:l.tag,payload:l.payload,callback:l.callback,next:null},c===null?(a=c=S,u=p):c=c.next=S,s|=m;if(l=l.next,l===null){if(l=i.shared.pending,l===null)break;m=l,l=m.next,m.next=null,i.lastBaseUpdate=m,i.shared.pending=null}}while(1);if(c===null&&(u=p),i.baseState=u,i.firstBaseUpdate=a,i.lastBaseUpdate=c,t=i.shared.interleaved,t!==null){i=t;do s|=i.lane,i=i.next;while(i!==t)}else o===null&&(i.shared.lanes=0);Ht|=s,e.lanes=s,e.memoizedState=p}}function qu(e,t,n){if(e=t.effects,t.effects=null,e!==null)for(t=0;tn?n:4,e(!0);var r=Fo.transition;Fo.transition={};try{e(!1),t()}finally{B=n,Fo.transition=r}}function pf(){return De().memoizedState}function Th(e,t,n){var r=kt(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},hf(e))mf(t,n);else if(n=Xc(e,t,n,r),n!==null){var i=de();$e(n,e,r,i),yf(n,t,r)}}function Nh(e,t,n){var r=kt(e),i={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(hf(e))mf(t,i);else{var o=e.alternate;if(e.lanes===0&&(o===null||o.lanes===0)&&(o=t.lastRenderedReducer,o!==null))try{var s=t.lastRenderedState,l=o(s,n);if(i.hasEagerState=!0,i.eagerState=l,He(l,s)){var u=t.interleaved;u===null?(i.next=i,Rl(t)):(i.next=u.next,u.next=i),t.interleaved=i;return}}catch{}finally{}n=Xc(e,t,i,r),n!==null&&(i=de(),$e(n,e,r,i),yf(n,t,r))}}function hf(e){var t=e.alternate;return e===H||t!==null&&t===H}function mf(e,t){Jn=Li=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function yf(e,t,n){if(n&4194240){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,dl(e,n)}}var Ai={readContext:Ae,useCallback:se,useContext:se,useEffect:se,useImperativeHandle:se,useInsertionEffect:se,useLayoutEffect:se,useMemo:se,useReducer:se,useRef:se,useState:se,useDebugValue:se,useDeferredValue:se,useTransition:se,useMutableSource:se,useSyncExternalStore:se,useId:se,unstable_isNewReconciler:!1},Oh={readContext:Ae,useCallback:function(e,t){return Ke().memoizedState=[e,t===void 0?null:t],e},useContext:Ae,useEffect:Ku,useImperativeHandle:function(e,t,n){return n=n!=null?n.concat([e]):null,ei(4194308,4,uf.bind(null,t,e),n)},useLayoutEffect:function(e,t){return ei(4194308,4,e,t)},useInsertionEffect:function(e,t){return ei(4,2,e,t)},useMemo:function(e,t){var n=Ke();return t=t===void 0?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ke();return t=n!==void 0?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=Th.bind(null,H,e),[r.memoizedState,e]},useRef:function(e){var t=Ke();return e={current:e},t.memoizedState=e},useState:Qu,useDebugValue:Fl,useDeferredValue:function(e){return Ke().memoizedState=e},useTransition:function(){var e=Qu(!1),t=e[0];return e=Rh.bind(null,e[1]),Ke().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=H,i=Ke();if($){if(n===void 0)throw Error(E(407));n=n()}else{if(n=t(),ne===null)throw Error(E(349));Vt&30||bc(r,t,n)}i.memoizedState=n;var o={value:n,getSnapshot:t};return i.queue=o,Ku(tf.bind(null,r,o,e),[e]),r.flags|=2048,mr(9,ef.bind(null,r,o,n,t),void 0,null),n},useId:function(){var e=Ke(),t=ne.identifierPrefix;if($){var n=nt,r=tt;n=(r&~(1<<32-je(r)-1)).toString(32)+n,t=":"+t+"R"+n,n=pr++,0<\/script>",e=e.removeChild(e.firstChild)):typeof r.is=="string"?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),n==="select"&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[Xe]=t,e[cr]=r,Rf(e,t,!1,!1),t.stateNode=e;e:{switch(s=ls(n,r),n){case"dialog":I("cancel",e),I("close",e),i=r;break;case"iframe":case"object":case"embed":I("load",e),i=r;break;case"video":case"audio":for(i=0;iEn&&(t.flags|=128,r=!0,Bn(o,!1),t.lanes=4194304)}else{if(!r)if(e=Pi(s),e!==null){if(t.flags|=128,r=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),Bn(o,!0),o.tail===null&&o.tailMode==="hidden"&&!s.alternate&&!$)return le(t),null}else 2*K()-o.renderingStartTime>En&&n!==1073741824&&(t.flags|=128,r=!0,Bn(o,!1),t.lanes=4194304);o.isBackwards?(s.sibling=t.child,t.child=s):(n=o.last,n!==null?n.sibling=s:t.child=s,o.last=s)}return o.tail!==null?(t=o.tail,o.rendering=t,o.tail=t.sibling,o.renderingStartTime=K(),t.sibling=null,n=V.current,U(V,r?n&1|2:n&1),t):(le(t),null);case 22:case 23:return $l(),r=t.memoizedState!==null,e!==null&&e.memoizedState!==null!==r&&(t.flags|=8192),r&&t.mode&1?Ee&1073741824&&(le(t),t.subtreeFlags&6&&(t.flags|=8192)):le(t),null;case 24:return null;case 25:return null}throw Error(E(156,t.tag))}function Uh(e,t){switch(El(t),t.tag){case 1:return ve(t.type)&&_i(),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return wn(),M(ge),M(ce),Pl(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 5:return Ol(t),null;case 13:if(M(V),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(E(340));gn()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return M(V),null;case 4:return wn(),null;case 10:return Cl(t.type._context),null;case 22:case 23:return $l(),null;case 24:return null;default:return null}}var Vr=!1,ue=!1,Ih=typeof WeakSet=="function"?WeakSet:Set,C=null;function sn(e,t){var n=e.ref;if(n!==null)if(typeof n=="function")try{n(null)}catch(r){q(e,t,r)}else n.current=null}function zs(e,t,n){try{n()}catch(r){q(e,t,r)}}var ia=!1;function Mh(e,t){if(gs=wi,e=Ac(),wl(e)){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var i=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break e}var s=0,l=-1,u=-1,a=0,c=0,p=e,m=null;t:for(;;){for(var S;p!==n||i!==0&&p.nodeType!==3||(l=s+i),p!==o||r!==0&&p.nodeType!==3||(u=s+r),p.nodeType===3&&(s+=p.nodeValue.length),(S=p.firstChild)!==null;)m=p,p=S;for(;;){if(p===e)break t;if(m===n&&++a===i&&(l=s),m===o&&++c===r&&(u=s),(S=p.nextSibling)!==null)break;p=m,m=p.parentNode}p=S}n=l===-1||u===-1?null:{start:l,end:u}}else n=null}n=n||{start:0,end:0}}else n=null;for(vs={focusedElem:e,selectionRange:n},wi=!1,C=t;C!==null;)if(t=C,e=t.child,(t.subtreeFlags&1028)!==0&&e!==null)e.return=t,C=e;else for(;C!==null;){t=C;try{var g=t.alternate;if(t.flags&1024)switch(t.tag){case 0:case 11:case 15:break;case 1:if(g!==null){var v=g.memoizedProps,k=g.memoizedState,d=t.stateNode,f=d.getSnapshotBeforeUpdate(t.elementType===t.type?v:Be(t.type,v),k);d.__reactInternalSnapshotBeforeUpdate=f}break;case 3:var h=t.stateNode.containerInfo;h.nodeType===1?h.textContent="":h.nodeType===9&&h.documentElement&&h.removeChild(h.documentElement);break;case 5:case 6:case 4:case 17:break;default:throw Error(E(163))}}catch(w){q(t,t.return,w)}if(e=t.sibling,e!==null){e.return=t.return,C=e;break}C=t.return}return g=ia,ia=!1,g}function Gn(e,t,n){var r=t.updateQueue;if(r=r!==null?r.lastEffect:null,r!==null){var i=r=r.next;do{if((i.tag&e)===e){var o=i.destroy;i.destroy=void 0,o!==void 0&&zs(t,n,o)}i=i.next}while(i!==r)}}function Ji(e,t){if(t=t.updateQueue,t=t!==null?t.lastEffect:null,t!==null){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function Fs(e){var t=e.ref;if(t!==null){var n=e.stateNode;switch(e.tag){case 5:e=n;break;default:e=n}typeof t=="function"?t(e):t.current=e}}function Of(e){var t=e.alternate;t!==null&&(e.alternate=null,Of(t)),e.child=null,e.deletions=null,e.sibling=null,e.tag===5&&(t=e.stateNode,t!==null&&(delete t[Xe],delete t[cr],delete t[Es],delete t[Eh],delete t[kh])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function Pf(e){return e.tag===5||e.tag===3||e.tag===4}function oa(e){e:for(;;){for(;e.sibling===null;){if(e.return===null||Pf(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.flags&2||e.child===null||e.tag===4)continue e;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Bs(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=ki));else if(r!==4&&(e=e.child,e!==null))for(Bs(e,t,n),e=e.sibling;e!==null;)Bs(e,t,n),e=e.sibling}function Us(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(e=e.child,e!==null))for(Us(e,t,n),e=e.sibling;e!==null;)Us(e,t,n),e=e.sibling}var re=null,Ue=!1;function at(e,t,n){for(n=n.child;n!==null;)Lf(e,t,n),n=n.sibling}function Lf(e,t,n){if(Ye&&typeof Ye.onCommitFiberUnmount=="function")try{Ye.onCommitFiberUnmount(Vi,n)}catch{}switch(n.tag){case 5:ue||sn(n,t);case 6:var r=re,i=Ue;re=null,at(e,t,n),re=r,Ue=i,re!==null&&(Ue?(e=re,n=n.stateNode,e.nodeType===8?e.parentNode.removeChild(n):e.removeChild(n)):re.removeChild(n.stateNode));break;case 18:re!==null&&(Ue?(e=re,n=n.stateNode,e.nodeType===8?Ao(e.parentNode,n):e.nodeType===1&&Ao(e,n),or(e)):Ao(re,n.stateNode));break;case 4:r=re,i=Ue,re=n.stateNode.containerInfo,Ue=!0,at(e,t,n),re=r,Ue=i;break;case 0:case 11:case 14:case 15:if(!ue&&(r=n.updateQueue,r!==null&&(r=r.lastEffect,r!==null))){i=r=r.next;do{var o=i,s=o.destroy;o=o.tag,s!==void 0&&(o&2||o&4)&&zs(n,t,s),i=i.next}while(i!==r)}at(e,t,n);break;case 1:if(!ue&&(sn(n,t),r=n.stateNode,typeof r.componentWillUnmount=="function"))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(l){q(n,t,l)}at(e,t,n);break;case 21:at(e,t,n);break;case 22:n.mode&1?(ue=(r=ue)||n.memoizedState!==null,at(e,t,n),ue=r):at(e,t,n);break;default:at(e,t,n)}}function sa(e){var t=e.updateQueue;if(t!==null){e.updateQueue=null;var n=e.stateNode;n===null&&(n=e.stateNode=new Ih),t.forEach(function(r){var i=Xh.bind(null,e,r);n.has(r)||(n.add(r),r.then(i,i))})}}function Fe(e,t){var n=t.deletions;if(n!==null)for(var r=0;ri&&(i=s),r&=~o}if(r=i,r=K()-r,r=(120>r?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*$h(r/1960))-r,10e?16:e,mt===null)var r=!1;else{if(e=mt,mt=null,Fi=0,F&6)throw Error(E(331));var i=F;for(F|=4,C=e.current;C!==null;){var o=C,s=o.child;if(C.flags&16){var l=o.deletions;if(l!==null){for(var u=0;uK()-Ml?It(e,0):Il|=n),we(e,t)}function Mf(e,t){t===0&&(e.mode&1?(t=Dr,Dr<<=1,!(Dr&130023424)&&(Dr=4194304)):t=1);var n=de();e=st(e,t),e!==null&&(wr(e,t,n),we(e,n))}function Kh(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),Mf(e,n)}function Xh(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(E(314))}r!==null&&r.delete(t),Mf(e,n)}var jf;jf=function(e,t,n){if(e!==null)if(e.memoizedProps!==t.pendingProps||ge.current)ye=!0;else{if(!(e.lanes&n)&&!(t.flags&128))return ye=!1,Fh(e,t,n);ye=!!(e.flags&131072)}else ye=!1,$&&t.flags&1048576&&Hc(t,Ri,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;ti(e,t),e=t.pendingProps;var i=yn(t,ce.current);dn(t,n),i=Al(null,t,r,e,i,n);var o=Dl();return t.flags|=1,typeof i=="object"&&i!==null&&typeof i.render=="function"&&i.$$typeof===void 0?(t.tag=1,t.memoizedState=null,t.updateQueue=null,ve(r)?(o=!0,xi(t)):o=!1,t.memoizedState=i.state!==null&&i.state!==void 0?i.state:null,Tl(t),i.updater=Yi,t.stateNode=i,i._reactInternals=t,Ts(t,r,e,n),t=Ps(null,t,r,!0,o,n)):(t.tag=0,$&&o&&Sl(t),fe(null,t,i,n),t=t.child),t;case 16:r=t.elementType;e:{switch(ti(e,t),e=t.pendingProps,i=r._init,r=i(r._payload),t.type=r,i=t.tag=Jh(r),e=Be(r,e),i){case 0:t=Os(null,t,r,e,n);break e;case 1:t=ta(null,t,r,e,n);break e;case 11:t=bu(null,t,r,e,n);break e;case 14:t=ea(null,t,r,Be(r.type,e),n);break e}throw Error(E(306,r,""))}return t;case 0:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Be(r,i),Os(e,t,r,i,n);case 1:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Be(r,i),ta(e,t,r,i,n);case 3:e:{if(_f(t),e===null)throw Error(E(387));r=t.pendingProps,o=t.memoizedState,i=o.element,Yc(e,t),Oi(t,r,null,n);var s=t.memoizedState;if(r=s.element,o.isDehydrated)if(o={element:r,isDehydrated:!1,cache:s.cache,pendingSuspenseBoundaries:s.pendingSuspenseBoundaries,transitions:s.transitions},t.updateQueue.baseState=o,t.memoizedState=o,t.flags&256){i=Sn(Error(E(423)),t),t=na(e,t,r,n,i);break e}else if(r!==i){i=Sn(Error(E(424)),t),t=na(e,t,r,n,i);break e}else for(ke=wt(t.stateNode.containerInfo.firstChild),_e=t,$=!0,Me=null,n=Kc(t,null,r,n),t.child=n;n;)n.flags=n.flags&-3|4096,n=n.sibling;else{if(gn(),r===i){t=lt(e,t,n);break e}fe(e,t,r,n)}t=t.child}return t;case 5:return Jc(t),e===null&&xs(t),r=t.type,i=t.pendingProps,o=e!==null?e.memoizedProps:null,s=i.children,ws(r,i)?s=null:o!==null&&ws(r,o)&&(t.flags|=32),kf(e,t),fe(e,t,s,n),t.child;case 6:return e===null&&xs(t),null;case 13:return xf(e,t,n);case 4:return Nl(t,t.stateNode.containerInfo),r=t.pendingProps,e===null?t.child=vn(t,null,r,n):fe(e,t,r,n),t.child;case 11:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Be(r,i),bu(e,t,r,i,n);case 7:return fe(e,t,t.pendingProps,n),t.child;case 8:return fe(e,t,t.pendingProps.children,n),t.child;case 12:return fe(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,i=t.pendingProps,o=t.memoizedProps,s=i.value,U(Ti,r._currentValue),r._currentValue=s,o!==null)if(He(o.value,s)){if(o.children===i.children&&!ge.current){t=lt(e,t,n);break e}}else for(o=t.child,o!==null&&(o.return=t);o!==null;){var l=o.dependencies;if(l!==null){s=o.child;for(var u=l.firstContext;u!==null;){if(u.context===r){if(o.tag===1){u=rt(-1,n&-n),u.tag=2;var a=o.updateQueue;if(a!==null){a=a.shared;var c=a.pending;c===null?u.next=u:(u.next=c.next,c.next=u),a.pending=u}}o.lanes|=n,u=o.alternate,u!==null&&(u.lanes|=n),Cs(o.return,n,t),l.lanes|=n;break}u=u.next}}else if(o.tag===10)s=o.type===t.type?null:o.child;else if(o.tag===18){if(s=o.return,s===null)throw Error(E(341));s.lanes|=n,l=s.alternate,l!==null&&(l.lanes|=n),Cs(s,n,t),s=o.sibling}else s=o.child;if(s!==null)s.return=o;else for(s=o;s!==null;){if(s===t){s=null;break}if(o=s.sibling,o!==null){o.return=s.return,s=o;break}s=s.return}o=s}fe(e,t,i.children,n),t=t.child}return t;case 9:return i=t.type,r=t.pendingProps.children,dn(t,n),i=Ae(i),r=r(i),t.flags|=1,fe(e,t,r,n),t.child;case 14:return r=t.type,i=Be(r,t.pendingProps),i=Be(r.type,i),ea(e,t,r,i,n);case 15:return Sf(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,i=t.pendingProps,i=t.elementType===r?i:Be(r,i),ti(e,t),t.tag=1,ve(r)?(e=!0,xi(t)):e=!1,dn(t,n),gf(t,r,i),Ts(t,r,i,n),Ps(null,t,r,!0,e,n);case 19:return Cf(e,t,n);case 22:return Ef(e,t,n)}throw Error(E(156,t.tag))};function $f(e,t){return pc(e,t)}function Yh(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Pe(e,t,n,r){return new Yh(e,t,n,r)}function Hl(e){return e=e.prototype,!(!e||!e.isReactComponent)}function Jh(e){if(typeof e=="function")return Hl(e)?1:0;if(e!=null){if(e=e.$$typeof,e===ul)return 11;if(e===al)return 14}return 2}function _t(e,t){var n=e.alternate;return n===null?(n=Pe(e.tag,t,e.key,e.mode),n.elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=e.flags&14680064,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=t===null?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function ii(e,t,n,r,i,o){var s=2;if(r=e,typeof e=="function")Hl(e)&&(s=1);else if(typeof e=="string")s=5;else e:switch(e){case Jt:return Mt(n.children,i,o,t);case ll:s=8,i|=8;break;case Go:return e=Pe(12,n,t,i|2),e.elementType=Go,e.lanes=o,e;case Zo:return e=Pe(13,n,t,i),e.elementType=Zo,e.lanes=o,e;case bo:return e=Pe(19,n,t,i),e.elementType=bo,e.lanes=o,e;case Ja:return Zi(n,i,o,t);default:if(typeof e=="object"&&e!==null)switch(e.$$typeof){case Xa:s=10;break e;case Ya:s=9;break e;case ul:s=11;break e;case al:s=14;break e;case ct:s=16,r=null;break e}throw Error(E(130,e==null?e:typeof e,""))}return t=Pe(s,n,t,i),t.elementType=e,t.type=r,t.lanes=o,t}function Mt(e,t,n,r){return e=Pe(7,e,r,t),e.lanes=n,e}function Zi(e,t,n,r){return e=Pe(22,e,r,t),e.elementType=Ja,e.lanes=n,e.stateNode={isHidden:!1},e}function jo(e,t,n){return e=Pe(6,e,null,t),e.lanes=n,e}function $o(e,t,n){return t=Pe(4,e.children!==null?e.children:[],e.key,t),t.lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Gh(e,t,n,r,i){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=Eo(0),this.expirationTimes=Eo(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=Eo(0),this.identifierPrefix=r,this.onRecoverableError=i,this.mutableSourceEagerHydrationData=null}function Wl(e,t,n,r,i,o,s,l,u){return e=new Gh(e,t,n,l,u),t===1?(t=1,o===!0&&(t|=8)):t=0,o=Pe(3,null,null,t),e.current=o,o.stateNode=e,o.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Tl(o),e}function Zh(e,t,n){var r=3"u"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(qf)}catch(e){console.error(e)}}qf(),Wa.exports=Ce;var rm=Wa.exports,ha=rm;Yo.createRoot=ha.createRoot,Yo.hydrateRoot=ha.hydrateRoot;function Qf(e,t){return function(){return e.apply(t,arguments)}}const{toString:im}=Object.prototype,{getPrototypeOf:Xl}=Object,{iterator:ro,toStringTag:Kf}=Symbol,io=(e=>t=>{const n=im.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),We=e=>(e=e.toLowerCase(),t=>io(t)===e),oo=e=>t=>typeof t===e,{isArray:Cn}=Array,gr=oo("undefined");function _r(e){return e!==null&&!gr(e)&&e.constructor!==null&&!gr(e.constructor)&&Se(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const Xf=We("ArrayBuffer");function om(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&Xf(e.buffer),t}const sm=oo("string"),Se=oo("function"),Yf=oo("number"),xr=e=>e!==null&&typeof e=="object",lm=e=>e===!0||e===!1,oi=e=>{if(io(e)!=="object")return!1;const t=Xl(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Kf in e)&&!(ro in e)},um=e=>{if(!xr(e)||_r(e))return!1;try{return Object.keys(e).length===0&&Object.getPrototypeOf(e)===Object.prototype}catch{return!1}},am=We("Date"),cm=We("File"),fm=We("Blob"),dm=We("FileList"),pm=e=>xr(e)&&Se(e.pipe),hm=e=>{let t;return e&&(typeof FormData=="function"&&e instanceof FormData||Se(e.append)&&((t=io(e))==="formdata"||t==="object"&&Se(e.toString)&&e.toString()==="[object FormData]"))},mm=We("URLSearchParams"),[ym,gm,vm,wm]=["ReadableStream","Request","Response","Headers"].map(We),Sm=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function Cr(e,t,{allOwnKeys:n=!1}={}){if(e===null||typeof e>"u")return;let r,i;if(typeof e!="object"&&(e=[e]),Cn(e))for(r=0,i=e.length;r0;)if(i=n[r],t===i.toLowerCase())return i;return null}const Ut=(()=>typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global)(),Gf=e=>!gr(e)&&e!==Ut;function Vs(){const{caseless:e}=Gf(this)&&this||{},t={},n=(r,i)=>{const o=e&&Jf(t,i)||i;oi(t[o])&&oi(r)?t[o]=Vs(t[o],r):oi(r)?t[o]=Vs({},r):Cn(r)?t[o]=r.slice():t[o]=r};for(let r=0,i=arguments.length;r(Cr(t,(i,o)=>{n&&Se(i)?e[o]=Qf(i,n):e[o]=i},{allOwnKeys:r}),e),km=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),_m=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},xm=(e,t,n,r)=>{let i,o,s;const l={};if(t=t||{},e==null)return t;do{for(i=Object.getOwnPropertyNames(e),o=i.length;o-- >0;)s=i[o],(!r||r(s,e,t))&&!l[s]&&(t[s]=e[s],l[s]=!0);e=n!==!1&&Xl(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},Cm=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return r!==-1&&r===n},Rm=e=>{if(!e)return null;if(Cn(e))return e;let t=e.length;if(!Yf(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},Tm=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&Xl(Uint8Array)),Nm=(e,t)=>{const r=(e&&e[ro]).call(e);let i;for(;(i=r.next())&&!i.done;){const o=i.value;t.call(e,o[0],o[1])}},Om=(e,t)=>{let n;const r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},Pm=We("HTMLFormElement"),Lm=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,r,i){return r.toUpperCase()+i}),ma=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),Am=We("RegExp"),Zf=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};Cr(n,(i,o)=>{let s;(s=t(i,o,e))!==!1&&(r[o]=s||i)}),Object.defineProperties(e,r)},Dm=e=>{Zf(e,(t,n)=>{if(Se(e)&&["arguments","caller","callee"].indexOf(n)!==-1)return!1;const r=e[n];if(Se(r)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")})}})},zm=(e,t)=>{const n={},r=i=>{i.forEach(o=>{n[o]=!0})};return Cn(e)?r(e):r(String(e).split(t)),n},Fm=()=>{},Bm=(e,t)=>e!=null&&Number.isFinite(e=+e)?e:t;function Um(e){return!!(e&&Se(e.append)&&e[Kf]==="FormData"&&e[ro])}const Im=e=>{const t=new Array(10),n=(r,i)=>{if(xr(r)){if(t.indexOf(r)>=0)return;if(_r(r))return r;if(!("toJSON"in r)){t[i]=r;const o=Cn(r)?[]:{};return Cr(r,(s,l)=>{const u=n(s,i+1);!gr(u)&&(o[l]=u)}),t[i]=void 0,o}}return r};return n(e,0)},Mm=We("AsyncFunction"),jm=e=>e&&(xr(e)||Se(e))&&Se(e.then)&&Se(e.catch),bf=((e,t)=>e?setImmediate:t?((n,r)=>(Ut.addEventListener("message",({source:i,data:o})=>{i===Ut&&o===n&&r.length&&r.shift()()},!1),i=>{r.push(i),Ut.postMessage(n,"*")}))(`axios@${Math.random()}`,[]):n=>setTimeout(n))(typeof setImmediate=="function",Se(Ut.postMessage)),$m=typeof queueMicrotask<"u"?queueMicrotask.bind(Ut):typeof process<"u"&&process.nextTick||bf,Vm=e=>e!=null&&Se(e[ro]),y={isArray:Cn,isArrayBuffer:Xf,isBuffer:_r,isFormData:hm,isArrayBufferView:om,isString:sm,isNumber:Yf,isBoolean:lm,isObject:xr,isPlainObject:oi,isEmptyObject:um,isReadableStream:ym,isRequest:gm,isResponse:vm,isHeaders:wm,isUndefined:gr,isDate:am,isFile:cm,isBlob:fm,isRegExp:Am,isFunction:Se,isStream:pm,isURLSearchParams:mm,isTypedArray:Tm,isFileList:dm,forEach:Cr,merge:Vs,extend:Em,trim:Sm,stripBOM:km,inherits:_m,toFlatObject:xm,kindOf:io,kindOfTest:We,endsWith:Cm,toArray:Rm,forEachEntry:Nm,matchAll:Om,isHTMLForm:Pm,hasOwnProperty:ma,hasOwnProp:ma,reduceDescriptors:Zf,freezeMethods:Dm,toObjectSet:zm,toCamelCase:Lm,noop:Fm,toFiniteNumber:Bm,findKey:Jf,global:Ut,isContextDefined:Gf,isSpecCompliantForm:Um,toJSONObject:Im,isAsyncFn:Mm,isThenable:jm,setImmediate:bf,asap:$m,isIterable:Vm};function O(e,t,n,r,i){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),i&&(this.response=i,this.status=i.status?i.status:null)}y.inherits(O,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:y.toJSONObject(this.config),code:this.code,status:this.status}}});const ed=O.prototype,td={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach(e=>{td[e]={value:e}});Object.defineProperties(O,td);Object.defineProperty(ed,"isAxiosError",{value:!0});O.from=(e,t,n,r,i,o)=>{const s=Object.create(ed);return y.toFlatObject(e,s,function(u){return u!==Error.prototype},l=>l!=="isAxiosError"),O.call(s,e.message,t,n,r,i),s.cause=e,s.name=e.name,o&&Object.assign(s,o),s};const Hm=null;function Hs(e){return y.isPlainObject(e)||y.isArray(e)}function nd(e){return y.endsWith(e,"[]")?e.slice(0,-2):e}function ya(e,t,n){return e?e.concat(t).map(function(i,o){return i=nd(i),!n&&o?"["+i+"]":i}).join(n?".":""):t}function Wm(e){return y.isArray(e)&&!e.some(Hs)}const qm=y.toFlatObject(y,{},null,function(t){return/^is[A-Z]/.test(t)});function so(e,t,n){if(!y.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,n=y.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(v,k){return!y.isUndefined(k[v])});const r=n.metaTokens,i=n.visitor||c,o=n.dots,s=n.indexes,u=(n.Blob||typeof Blob<"u"&&Blob)&&y.isSpecCompliantForm(t);if(!y.isFunction(i))throw new TypeError("visitor must be a function");function a(g){if(g===null)return"";if(y.isDate(g))return g.toISOString();if(y.isBoolean(g))return g.toString();if(!u&&y.isBlob(g))throw new O("Blob is not supported. Use a Buffer instead.");return y.isArrayBuffer(g)||y.isTypedArray(g)?u&&typeof Blob=="function"?new Blob([g]):Buffer.from(g):g}function c(g,v,k){let d=g;if(g&&!k&&typeof g=="object"){if(y.endsWith(v,"{}"))v=r?v:v.slice(0,-2),g=JSON.stringify(g);else if(y.isArray(g)&&Wm(g)||(y.isFileList(g)||y.endsWith(v,"[]"))&&(d=y.toArray(g)))return v=nd(v),d.forEach(function(h,w){!(y.isUndefined(h)||h===null)&&t.append(s===!0?ya([v],w,o):s===null?v:v+"[]",a(h))}),!1}return Hs(g)?!0:(t.append(ya(k,v,o),a(g)),!1)}const p=[],m=Object.assign(qm,{defaultVisitor:c,convertValue:a,isVisitable:Hs});function S(g,v){if(!y.isUndefined(g)){if(p.indexOf(g)!==-1)throw Error("Circular reference detected in "+v.join("."));p.push(g),y.forEach(g,function(d,f){(!(y.isUndefined(d)||d===null)&&i.call(t,d,y.isString(f)?f.trim():f,v,m))===!0&&S(d,v?v.concat(f):[f])}),p.pop()}}if(!y.isObject(e))throw new TypeError("data must be an object");return S(e),t}function ga(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(r){return t[r]})}function Yl(e,t){this._pairs=[],e&&so(e,this,t)}const rd=Yl.prototype;rd.append=function(t,n){this._pairs.push([t,n])};rd.toString=function(t){const n=t?function(r){return t.call(this,r,ga)}:ga;return this._pairs.map(function(i){return n(i[0])+"="+n(i[1])},"").join("&")};function Qm(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function id(e,t,n){if(!t)return e;const r=n&&n.encode||Qm;y.isFunction(n)&&(n={serialize:n});const i=n&&n.serialize;let o;if(i?o=i(t,n):o=y.isURLSearchParams(t)?t.toString():new Yl(t,n).toString(r),o){const s=e.indexOf("#");s!==-1&&(e=e.slice(0,s)),e+=(e.indexOf("?")===-1?"?":"&")+o}return e}class Km{constructor(){this.handlers=[]}use(t,n,r){return this.handlers.push({fulfilled:t,rejected:n,synchronous:r?r.synchronous:!1,runWhen:r?r.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){y.forEach(this.handlers,function(r){r!==null&&t(r)})}}const va=Km,od={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},Xm=typeof URLSearchParams<"u"?URLSearchParams:Yl,Ym=typeof FormData<"u"?FormData:null,Jm=typeof Blob<"u"?Blob:null,Gm={isBrowser:!0,classes:{URLSearchParams:Xm,FormData:Ym,Blob:Jm},protocols:["http","https","file","blob","url","data"]},Jl=typeof window<"u"&&typeof document<"u",Ws=typeof navigator=="object"&&navigator||void 0,Zm=Jl&&(!Ws||["ReactNative","NativeScript","NS"].indexOf(Ws.product)<0),bm=(()=>typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function")(),ey=Jl&&window.location.href||"http://localhost",ty=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:Jl,hasStandardBrowserEnv:Zm,hasStandardBrowserWebWorkerEnv:bm,navigator:Ws,origin:ey},Symbol.toStringTag,{value:"Module"})),ae={...ty,...Gm};function ny(e,t){return so(e,new ae.classes.URLSearchParams,{visitor:function(n,r,i,o){return ae.isNode&&y.isBuffer(n)?(this.append(r,n.toString("base64")),!1):o.defaultVisitor.apply(this,arguments)},...t})}function ry(e){return y.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"":t[1]||t[0])}function iy(e){const t={},n=Object.keys(e);let r;const i=n.length;let o;for(r=0;r=n.length;return s=!s&&y.isArray(i)?i.length:s,u?(y.hasOwnProp(i,s)?i[s]=[i[s],r]:i[s]=r,!l):((!i[s]||!y.isObject(i[s]))&&(i[s]=[]),t(n,r,i[s],o)&&y.isArray(i[s])&&(i[s]=iy(i[s])),!l)}if(y.isFormData(e)&&y.isFunction(e.entries)){const n={};return y.forEachEntry(e,(r,i)=>{t(ry(r),i,n,0)}),n}return null}function oy(e,t,n){if(y.isString(e))try{return(t||JSON.parse)(e),y.trim(e)}catch(r){if(r.name!=="SyntaxError")throw r}return(n||JSON.stringify)(e)}const Gl={transitional:od,adapter:["xhr","http","fetch"],transformRequest:[function(t,n){const r=n.getContentType()||"",i=r.indexOf("application/json")>-1,o=y.isObject(t);if(o&&y.isHTMLForm(t)&&(t=new FormData(t)),y.isFormData(t))return i?JSON.stringify(sd(t)):t;if(y.isArrayBuffer(t)||y.isBuffer(t)||y.isStream(t)||y.isFile(t)||y.isBlob(t)||y.isReadableStream(t))return t;if(y.isArrayBufferView(t))return t.buffer;if(y.isURLSearchParams(t))return n.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let l;if(o){if(r.indexOf("application/x-www-form-urlencoded")>-1)return ny(t,this.formSerializer).toString();if((l=y.isFileList(t))||r.indexOf("multipart/form-data")>-1){const u=this.env&&this.env.FormData;return so(l?{"files[]":t}:t,u&&new u,this.formSerializer)}}return o||i?(n.setContentType("application/json",!1),oy(t)):t}],transformResponse:[function(t){const n=this.transitional||Gl.transitional,r=n&&n.forcedJSONParsing,i=this.responseType==="json";if(y.isResponse(t)||y.isReadableStream(t))return t;if(t&&y.isString(t)&&(r&&!this.responseType||i)){const s=!(n&&n.silentJSONParsing)&&i;try{return JSON.parse(t)}catch(l){if(s)throw l.name==="SyntaxError"?O.from(l,O.ERR_BAD_RESPONSE,this,null,this.response):l}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:ae.classes.FormData,Blob:ae.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};y.forEach(["delete","get","head","post","put","patch"],e=>{Gl.headers[e]={}});const Zl=Gl,sy=y.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),ly=e=>{const t={};let n,r,i;return e&&e.split(` +`).forEach(function(s){i=s.indexOf(":"),n=s.substring(0,i).trim().toLowerCase(),r=s.substring(i+1).trim(),!(!n||t[n]&&sy[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)}),t},wa=Symbol("internals");function In(e){return e&&String(e).trim().toLowerCase()}function si(e){return e===!1||e==null?e:y.isArray(e)?e.map(si):String(e)}function uy(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const ay=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function Vo(e,t,n,r,i){if(y.isFunction(r))return r.call(this,t,n);if(i&&(t=n),!!y.isString(t)){if(y.isString(r))return t.indexOf(r)!==-1;if(y.isRegExp(r))return r.test(t)}}function cy(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,n,r)=>n.toUpperCase()+r)}function fy(e,t){const n=y.toCamelCase(" "+t);["get","set","has"].forEach(r=>{Object.defineProperty(e,r+n,{value:function(i,o,s){return this[r].call(this,t,i,o,s)},configurable:!0})})}class lo{constructor(t){t&&this.set(t)}set(t,n,r){const i=this;function o(l,u,a){const c=In(u);if(!c)throw new Error("header name must be a non-empty string");const p=y.findKey(i,c);(!p||i[p]===void 0||a===!0||a===void 0&&i[p]!==!1)&&(i[p||u]=si(l))}const s=(l,u)=>y.forEach(l,(a,c)=>o(a,c,u));if(y.isPlainObject(t)||t instanceof this.constructor)s(t,n);else if(y.isString(t)&&(t=t.trim())&&!ay(t))s(ly(t),n);else if(y.isObject(t)&&y.isIterable(t)){let l={},u,a;for(const c of t){if(!y.isArray(c))throw TypeError("Object iterator must return a key-value pair");l[a=c[0]]=(u=l[a])?y.isArray(u)?[...u,c[1]]:[u,c[1]]:c[1]}s(l,n)}else t!=null&&o(n,t,r);return this}get(t,n){if(t=In(t),t){const r=y.findKey(this,t);if(r){const i=this[r];if(!n)return i;if(n===!0)return uy(i);if(y.isFunction(n))return n.call(this,i,r);if(y.isRegExp(n))return n.exec(i);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,n){if(t=In(t),t){const r=y.findKey(this,t);return!!(r&&this[r]!==void 0&&(!n||Vo(this,this[r],r,n)))}return!1}delete(t,n){const r=this;let i=!1;function o(s){if(s=In(s),s){const l=y.findKey(r,s);l&&(!n||Vo(r,r[l],l,n))&&(delete r[l],i=!0)}}return y.isArray(t)?t.forEach(o):o(t),i}clear(t){const n=Object.keys(this);let r=n.length,i=!1;for(;r--;){const o=n[r];(!t||Vo(this,this[o],o,t,!0))&&(delete this[o],i=!0)}return i}normalize(t){const n=this,r={};return y.forEach(this,(i,o)=>{const s=y.findKey(r,o);if(s){n[s]=si(i),delete n[o];return}const l=t?cy(o):String(o).trim();l!==o&&delete n[o],n[l]=si(i),r[l]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const n=Object.create(null);return y.forEach(this,(r,i)=>{r!=null&&r!==!1&&(n[i]=t&&y.isArray(r)?r.join(", "):r)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).join(` +`)}getSetCookie(){return this.get("set-cookie")||[]}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const r=new this(t);return n.forEach(i=>r.set(i)),r}static accessor(t){const r=(this[wa]=this[wa]={accessors:{}}).accessors,i=this.prototype;function o(s){const l=In(s);r[l]||(fy(i,s),r[l]=!0)}return y.isArray(t)?t.forEach(o):o(t),this}}lo.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);y.reduceDescriptors(lo.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(r){this[n]=r}}});y.freezeMethods(lo);const Ve=lo;function Ho(e,t){const n=this||Zl,r=t||n,i=Ve.from(r.headers);let o=r.data;return y.forEach(e,function(l){o=l.call(n,o,i.normalize(),t?t.status:void 0)}),i.normalize(),o}function ld(e){return!!(e&&e.__CANCEL__)}function Rn(e,t,n){O.call(this,e??"canceled",O.ERR_CANCELED,t,n),this.name="CanceledError"}y.inherits(Rn,O,{__CANCEL__:!0});function ud(e,t,n){const r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new O("Request failed with status code "+n.status,[O.ERR_BAD_REQUEST,O.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}function dy(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function py(e,t){e=e||10;const n=new Array(e),r=new Array(e);let i=0,o=0,s;return t=t!==void 0?t:1e3,function(u){const a=Date.now(),c=r[o];s||(s=a),n[i]=u,r[i]=a;let p=o,m=0;for(;p!==i;)m+=n[p++],p=p%e;if(i=(i+1)%e,i===o&&(o=(o+1)%e),a-s{n=c,i=null,o&&(clearTimeout(o),o=null),e(...a)};return[(...a)=>{const c=Date.now(),p=c-n;p>=r?s(a,c):(i=a,o||(o=setTimeout(()=>{o=null,s(i)},r-p)))},()=>i&&s(i)]}const Ii=(e,t,n=3)=>{let r=0;const i=py(50,250);return hy(o=>{const s=o.loaded,l=o.lengthComputable?o.total:void 0,u=s-r,a=i(u),c=s<=l;r=s;const p={loaded:s,total:l,progress:l?s/l:void 0,bytes:u,rate:a||void 0,estimated:a&&l&&c?(l-s)/a:void 0,event:o,lengthComputable:l!=null,[t?"download":"upload"]:!0};e(p)},n)},Sa=(e,t)=>{const n=e!=null;return[r=>t[0]({lengthComputable:n,total:e,loaded:r}),t[1]]},Ea=e=>(...t)=>y.asap(()=>e(...t)),my=ae.hasStandardBrowserEnv?((e,t)=>n=>(n=new URL(n,ae.origin),e.protocol===n.protocol&&e.host===n.host&&(t||e.port===n.port)))(new URL(ae.origin),ae.navigator&&/(msie|trident)/i.test(ae.navigator.userAgent)):()=>!0,yy=ae.hasStandardBrowserEnv?{write(e,t,n,r,i,o){const s=[e+"="+encodeURIComponent(t)];y.isNumber(n)&&s.push("expires="+new Date(n).toGMTString()),y.isString(r)&&s.push("path="+r),y.isString(i)&&s.push("domain="+i),o===!0&&s.push("secure"),document.cookie=s.join("; ")},read(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove(e){this.write(e,"",Date.now()-864e5)}}:{write(){},read(){return null},remove(){}};function gy(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function vy(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function ad(e,t,n){let r=!gy(t);return e&&(r||n==!1)?vy(e,t):t}const ka=e=>e instanceof Ve?{...e}:e;function qt(e,t){t=t||{};const n={};function r(a,c,p,m){return y.isPlainObject(a)&&y.isPlainObject(c)?y.merge.call({caseless:m},a,c):y.isPlainObject(c)?y.merge({},c):y.isArray(c)?c.slice():c}function i(a,c,p,m){if(y.isUndefined(c)){if(!y.isUndefined(a))return r(void 0,a,p,m)}else return r(a,c,p,m)}function o(a,c){if(!y.isUndefined(c))return r(void 0,c)}function s(a,c){if(y.isUndefined(c)){if(!y.isUndefined(a))return r(void 0,a)}else return r(void 0,c)}function l(a,c,p){if(p in t)return r(a,c);if(p in e)return r(void 0,a)}const u={url:o,method:o,data:o,baseURL:s,transformRequest:s,transformResponse:s,paramsSerializer:s,timeout:s,timeoutMessage:s,withCredentials:s,withXSRFToken:s,adapter:s,responseType:s,xsrfCookieName:s,xsrfHeaderName:s,onUploadProgress:s,onDownloadProgress:s,decompress:s,maxContentLength:s,maxBodyLength:s,beforeRedirect:s,transport:s,httpAgent:s,httpsAgent:s,cancelToken:s,socketPath:s,responseEncoding:s,validateStatus:l,headers:(a,c,p)=>i(ka(a),ka(c),p,!0)};return y.forEach(Object.keys({...e,...t}),function(c){const p=u[c]||i,m=p(e[c],t[c],c);y.isUndefined(m)&&p!==l||(n[c]=m)}),n}const cd=e=>{const t=qt({},e);let{data:n,withXSRFToken:r,xsrfHeaderName:i,xsrfCookieName:o,headers:s,auth:l}=t;t.headers=s=Ve.from(s),t.url=id(ad(t.baseURL,t.url,t.allowAbsoluteUrls),e.params,e.paramsSerializer),l&&s.set("Authorization","Basic "+btoa((l.username||"")+":"+(l.password?unescape(encodeURIComponent(l.password)):"")));let u;if(y.isFormData(n)){if(ae.hasStandardBrowserEnv||ae.hasStandardBrowserWebWorkerEnv)s.setContentType(void 0);else if((u=s.getContentType())!==!1){const[a,...c]=u?u.split(";").map(p=>p.trim()).filter(Boolean):[];s.setContentType([a||"multipart/form-data",...c].join("; "))}}if(ae.hasStandardBrowserEnv&&(r&&y.isFunction(r)&&(r=r(t)),r||r!==!1&&my(t.url))){const a=i&&o&&yy.read(o);a&&s.set(i,a)}return t},wy=typeof XMLHttpRequest<"u",Sy=wy&&function(e){return new Promise(function(n,r){const i=cd(e);let o=i.data;const s=Ve.from(i.headers).normalize();let{responseType:l,onUploadProgress:u,onDownloadProgress:a}=i,c,p,m,S,g;function v(){S&&S(),g&&g(),i.cancelToken&&i.cancelToken.unsubscribe(c),i.signal&&i.signal.removeEventListener("abort",c)}let k=new XMLHttpRequest;k.open(i.method.toUpperCase(),i.url,!0),k.timeout=i.timeout;function d(){if(!k)return;const h=Ve.from("getAllResponseHeaders"in k&&k.getAllResponseHeaders()),_={data:!l||l==="text"||l==="json"?k.responseText:k.response,status:k.status,statusText:k.statusText,headers:h,config:e,request:k};ud(function(R){n(R),v()},function(R){r(R),v()},_),k=null}"onloadend"in k?k.onloadend=d:k.onreadystatechange=function(){!k||k.readyState!==4||k.status===0&&!(k.responseURL&&k.responseURL.indexOf("file:")===0)||setTimeout(d)},k.onabort=function(){k&&(r(new O("Request aborted",O.ECONNABORTED,e,k)),k=null)},k.onerror=function(){r(new O("Network Error",O.ERR_NETWORK,e,k)),k=null},k.ontimeout=function(){let w=i.timeout?"timeout of "+i.timeout+"ms exceeded":"timeout exceeded";const _=i.transitional||od;i.timeoutErrorMessage&&(w=i.timeoutErrorMessage),r(new O(w,_.clarifyTimeoutError?O.ETIMEDOUT:O.ECONNABORTED,e,k)),k=null},o===void 0&&s.setContentType(null),"setRequestHeader"in k&&y.forEach(s.toJSON(),function(w,_){k.setRequestHeader(_,w)}),y.isUndefined(i.withCredentials)||(k.withCredentials=!!i.withCredentials),l&&l!=="json"&&(k.responseType=i.responseType),a&&([m,g]=Ii(a,!0),k.addEventListener("progress",m)),u&&k.upload&&([p,S]=Ii(u),k.upload.addEventListener("progress",p),k.upload.addEventListener("loadend",S)),(i.cancelToken||i.signal)&&(c=h=>{k&&(r(!h||h.type?new Rn(null,e,k):h),k.abort(),k=null)},i.cancelToken&&i.cancelToken.subscribe(c),i.signal&&(i.signal.aborted?c():i.signal.addEventListener("abort",c)));const f=dy(i.url);if(f&&ae.protocols.indexOf(f)===-1){r(new O("Unsupported protocol "+f+":",O.ERR_BAD_REQUEST,e));return}k.send(o||null)})},Ey=(e,t)=>{const{length:n}=e=e?e.filter(Boolean):[];if(t||n){let r=new AbortController,i;const o=function(a){if(!i){i=!0,l();const c=a instanceof Error?a:this.reason;r.abort(c instanceof O?c:new Rn(c instanceof Error?c.message:c))}};let s=t&&setTimeout(()=>{s=null,o(new O(`timeout ${t} of ms exceeded`,O.ETIMEDOUT))},t);const l=()=>{e&&(s&&clearTimeout(s),s=null,e.forEach(a=>{a.unsubscribe?a.unsubscribe(o):a.removeEventListener("abort",o)}),e=null)};e.forEach(a=>a.addEventListener("abort",o));const{signal:u}=r;return u.unsubscribe=()=>y.asap(l),u}},ky=Ey,_y=function*(e,t){let n=e.byteLength;if(!t||n{const i=xy(e,t);let o=0,s,l=u=>{s||(s=!0,r&&r(u))};return new ReadableStream({async pull(u){try{const{done:a,value:c}=await i.next();if(a){l(),u.close();return}let p=c.byteLength;if(n){let m=o+=p;n(m)}u.enqueue(new Uint8Array(c))}catch(a){throw l(a),a}},cancel(u){return l(u),i.return()}},{highWaterMark:2})},uo=typeof fetch=="function"&&typeof Request=="function"&&typeof Response=="function",fd=uo&&typeof ReadableStream=="function",Ry=uo&&(typeof TextEncoder=="function"?(e=>t=>e.encode(t))(new TextEncoder):async e=>new Uint8Array(await new Response(e).arrayBuffer())),dd=(e,...t)=>{try{return!!e(...t)}catch{return!1}},Ty=fd&&dd(()=>{let e=!1;const t=new Request(ae.origin,{body:new ReadableStream,method:"POST",get duplex(){return e=!0,"half"}}).headers.has("Content-Type");return e&&!t}),xa=64*1024,qs=fd&&dd(()=>y.isReadableStream(new Response("").body)),Mi={stream:qs&&(e=>e.body)};uo&&(e=>{["text","arrayBuffer","blob","formData","stream"].forEach(t=>{!Mi[t]&&(Mi[t]=y.isFunction(e[t])?n=>n[t]():(n,r)=>{throw new O(`Response type '${t}' is not supported`,O.ERR_NOT_SUPPORT,r)})})})(new Response);const Ny=async e=>{if(e==null)return 0;if(y.isBlob(e))return e.size;if(y.isSpecCompliantForm(e))return(await new Request(ae.origin,{method:"POST",body:e}).arrayBuffer()).byteLength;if(y.isArrayBufferView(e)||y.isArrayBuffer(e))return e.byteLength;if(y.isURLSearchParams(e)&&(e=e+""),y.isString(e))return(await Ry(e)).byteLength},Oy=async(e,t)=>{const n=y.toFiniteNumber(e.getContentLength());return n??Ny(t)},Py=uo&&(async e=>{let{url:t,method:n,data:r,signal:i,cancelToken:o,timeout:s,onDownloadProgress:l,onUploadProgress:u,responseType:a,headers:c,withCredentials:p="same-origin",fetchOptions:m}=cd(e);a=a?(a+"").toLowerCase():"text";let S=ky([i,o&&o.toAbortSignal()],s),g;const v=S&&S.unsubscribe&&(()=>{S.unsubscribe()});let k;try{if(u&&Ty&&n!=="get"&&n!=="head"&&(k=await Oy(c,r))!==0){let _=new Request(t,{method:"POST",body:r,duplex:"half"}),x;if(y.isFormData(r)&&(x=_.headers.get("content-type"))&&c.setContentType(x),_.body){const[R,N]=Sa(k,Ii(Ea(u)));r=_a(_.body,xa,R,N)}}y.isString(p)||(p=p?"include":"omit");const d="credentials"in Request.prototype;g=new Request(t,{...m,signal:S,method:n.toUpperCase(),headers:c.normalize().toJSON(),body:r,duplex:"half",credentials:d?p:void 0});let f=await fetch(g,m);const h=qs&&(a==="stream"||a==="response");if(qs&&(l||h&&v)){const _={};["status","statusText","headers"].forEach(j=>{_[j]=f[j]});const x=y.toFiniteNumber(f.headers.get("content-length")),[R,N]=l&&Sa(x,Ii(Ea(l),!0))||[];f=new Response(_a(f.body,xa,R,()=>{N&&N(),v&&v()}),_)}a=a||"text";let w=await Mi[y.findKey(Mi,a)||"text"](f,e);return!h&&v&&v(),await new Promise((_,x)=>{ud(_,x,{data:w,headers:Ve.from(f.headers),status:f.status,statusText:f.statusText,config:e,request:g})})}catch(d){throw v&&v(),d&&d.name==="TypeError"&&/Load failed|fetch/i.test(d.message)?Object.assign(new O("Network Error",O.ERR_NETWORK,e,g),{cause:d.cause||d}):O.from(d,d&&d.code,e,g)}}),Qs={http:Hm,xhr:Sy,fetch:Py};y.forEach(Qs,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch{}Object.defineProperty(e,"adapterName",{value:t})}});const Ca=e=>`- ${e}`,Ly=e=>y.isFunction(e)||e===null||e===!1,pd={getAdapter:e=>{e=y.isArray(e)?e:[e];const{length:t}=e;let n,r;const i={};for(let o=0;o`adapter ${l} `+(u===!1?"is not supported by the environment":"is not available in the build"));let s=t?o.length>1?`since : +`+o.map(Ca).join(` +`):" "+Ca(o[0]):"as no adapter specified";throw new O("There is no suitable adapter to dispatch the request "+s,"ERR_NOT_SUPPORT")}return r},adapters:Qs};function Wo(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new Rn(null,e)}function Ra(e){return Wo(e),e.headers=Ve.from(e.headers),e.data=Ho.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),pd.getAdapter(e.adapter||Zl.adapter)(e).then(function(r){return Wo(e),r.data=Ho.call(e,e.transformResponse,r),r.headers=Ve.from(r.headers),r},function(r){return ld(r)||(Wo(e),r&&r.response&&(r.response.data=Ho.call(e,e.transformResponse,r.response),r.response.headers=Ve.from(r.response.headers))),Promise.reject(r)})}const hd="1.11.0",ao={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{ao[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}});const Ta={};ao.transitional=function(t,n,r){function i(o,s){return"[Axios v"+hd+"] Transitional option '"+o+"'"+s+(r?". "+r:"")}return(o,s,l)=>{if(t===!1)throw new O(i(s," has been removed"+(n?" in "+n:"")),O.ERR_DEPRECATED);return n&&!Ta[s]&&(Ta[s]=!0,console.warn(i(s," has been deprecated since v"+n+" and will be removed in the near future"))),t?t(o,s,l):!0}};ao.spelling=function(t){return(n,r)=>(console.warn(`${r} is likely a misspelling of ${t}`),!0)};function Ay(e,t,n){if(typeof e!="object")throw new O("options must be an object",O.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let i=r.length;for(;i-- >0;){const o=r[i],s=t[o];if(s){const l=e[o],u=l===void 0||s(l,o,e);if(u!==!0)throw new O("option "+o+" must be "+u,O.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new O("Unknown option "+o,O.ERR_BAD_OPTION)}}const li={assertOptions:Ay,validators:ao},Qe=li.validators;class ji{constructor(t){this.defaults=t||{},this.interceptors={request:new va,response:new va}}async request(t,n){try{return await this._request(t,n)}catch(r){if(r instanceof Error){let i={};Error.captureStackTrace?Error.captureStackTrace(i):i=new Error;const o=i.stack?i.stack.replace(/^.+\n/,""):"";try{r.stack?o&&!String(r.stack).endsWith(o.replace(/^.+\n.+\n/,""))&&(r.stack+=` +`+o):r.stack=o}catch{}}throw r}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=qt(this.defaults,n);const{transitional:r,paramsSerializer:i,headers:o}=n;r!==void 0&&li.assertOptions(r,{silentJSONParsing:Qe.transitional(Qe.boolean),forcedJSONParsing:Qe.transitional(Qe.boolean),clarifyTimeoutError:Qe.transitional(Qe.boolean)},!1),i!=null&&(y.isFunction(i)?n.paramsSerializer={serialize:i}:li.assertOptions(i,{encode:Qe.function,serialize:Qe.function},!0)),n.allowAbsoluteUrls!==void 0||(this.defaults.allowAbsoluteUrls!==void 0?n.allowAbsoluteUrls=this.defaults.allowAbsoluteUrls:n.allowAbsoluteUrls=!0),li.assertOptions(n,{baseUrl:Qe.spelling("baseURL"),withXsrfToken:Qe.spelling("withXSRFToken")},!0),n.method=(n.method||this.defaults.method||"get").toLowerCase();let s=o&&y.merge(o.common,o[n.method]);o&&y.forEach(["delete","get","head","post","put","patch","common"],g=>{delete o[g]}),n.headers=Ve.concat(s,o);const l=[];let u=!0;this.interceptors.request.forEach(function(v){typeof v.runWhen=="function"&&v.runWhen(n)===!1||(u=u&&v.synchronous,l.unshift(v.fulfilled,v.rejected))});const a=[];this.interceptors.response.forEach(function(v){a.push(v.fulfilled,v.rejected)});let c,p=0,m;if(!u){const g=[Ra.bind(this),void 0];for(g.unshift(...l),g.push(...a),m=g.length,c=Promise.resolve(n);p{if(!r._listeners)return;let o=r._listeners.length;for(;o-- >0;)r._listeners[o](i);r._listeners=null}),this.promise.then=i=>{let o;const s=new Promise(l=>{r.subscribe(l),o=l}).then(i);return s.cancel=function(){r.unsubscribe(o)},s},t(function(o,s,l){r.reason||(r.reason=new Rn(o,s,l),n(r.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}toAbortSignal(){const t=new AbortController,n=r=>{t.abort(r)};return this.subscribe(n),t.signal.unsubscribe=()=>this.unsubscribe(n),t.signal}static source(){let t;return{token:new bl(function(i){t=i}),cancel:t}}}const Dy=bl;function zy(e){return function(n){return e.apply(null,n)}}function Fy(e){return y.isObject(e)&&e.isAxiosError===!0}const Ks={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(Ks).forEach(([e,t])=>{Ks[t]=e});const By=Ks;function md(e){const t=new ui(e),n=Qf(ui.prototype.request,t);return y.extend(n,ui.prototype,t,{allOwnKeys:!0}),y.extend(n,t,null,{allOwnKeys:!0}),n.create=function(i){return md(qt(e,i))},n}const J=md(Zl);J.Axios=ui;J.CanceledError=Rn;J.CancelToken=Dy;J.isCancel=ld;J.VERSION=hd;J.toFormData=so;J.AxiosError=O;J.Cancel=J.CanceledError;J.all=function(t){return Promise.all(t)};J.spread=zy;J.isAxiosError=Fy;J.mergeConfig=qt;J.AxiosHeaders=Ve;J.formToJSON=e=>sd(y.isHTMLForm(e)?new FormData(e):e);J.getAdapter=pd.getAdapter;J.HttpStatusCode=By;J.default=J;const qo=J,Ge=Object.create(null);Ge.open="0";Ge.close="1";Ge.ping="2";Ge.pong="3";Ge.message="4";Ge.upgrade="5";Ge.noop="6";const ai=Object.create(null);Object.keys(Ge).forEach(e=>{ai[Ge[e]]=e});const Xs={type:"error",data:"parser error"},yd=typeof Blob=="function"||typeof Blob<"u"&&Object.prototype.toString.call(Blob)==="[object BlobConstructor]",gd=typeof ArrayBuffer=="function",vd=e=>typeof ArrayBuffer.isView=="function"?ArrayBuffer.isView(e):e&&e.buffer instanceof ArrayBuffer,eu=({type:e,data:t},n,r)=>yd&&t instanceof Blob?n?r(t):Na(t,r):gd&&(t instanceof ArrayBuffer||vd(t))?n?r(t):Na(new Blob([t]),r):r(Ge[e]+(t||"")),Na=(e,t)=>{const n=new FileReader;return n.onload=function(){const r=n.result.split(",")[1];t("b"+(r||""))},n.readAsDataURL(e)};function Oa(e){return e instanceof Uint8Array?e:e instanceof ArrayBuffer?new Uint8Array(e):new Uint8Array(e.buffer,e.byteOffset,e.byteLength)}let Qo;function Uy(e,t){if(yd&&e.data instanceof Blob)return e.data.arrayBuffer().then(Oa).then(t);if(gd&&(e.data instanceof ArrayBuffer||vd(e.data)))return t(Oa(e.data));eu(e,!1,n=>{Qo||(Qo=new TextEncoder),t(Qo.encode(n))})}const Pa="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Wn=typeof Uint8Array>"u"?[]:new Uint8Array(256);for(let e=0;e{let t=e.length*.75,n=e.length,r,i=0,o,s,l,u;e[e.length-1]==="="&&(t--,e[e.length-2]==="="&&t--);const a=new ArrayBuffer(t),c=new Uint8Array(a);for(r=0;r>4,c[i++]=(s&15)<<4|l>>2,c[i++]=(l&3)<<6|u&63;return a},My=typeof ArrayBuffer=="function",tu=(e,t)=>{if(typeof e!="string")return{type:"message",data:wd(e,t)};const n=e.charAt(0);return n==="b"?{type:"message",data:jy(e.substring(1),t)}:ai[n]?e.length>1?{type:ai[n],data:e.substring(1)}:{type:ai[n]}:Xs},jy=(e,t)=>{if(My){const n=Iy(e);return wd(n,t)}else return{base64:!0,data:e}},wd=(e,t)=>{switch(t){case"blob":return e instanceof Blob?e:new Blob([e]);case"arraybuffer":default:return e instanceof ArrayBuffer?e:e.buffer}},Sd=String.fromCharCode(30),$y=(e,t)=>{const n=e.length,r=new Array(n);let i=0;e.forEach((o,s)=>{eu(o,!1,l=>{r[s]=l,++i===n&&t(r.join(Sd))})})},Vy=(e,t)=>{const n=e.split(Sd),r=[];for(let i=0;i{const r=n.length;let i;if(r<126)i=new Uint8Array(1),new DataView(i.buffer).setUint8(0,r);else if(r<65536){i=new Uint8Array(3);const o=new DataView(i.buffer);o.setUint8(0,126),o.setUint16(1,r)}else{i=new Uint8Array(9);const o=new DataView(i.buffer);o.setUint8(0,127),o.setBigUint64(1,BigInt(r))}e.data&&typeof e.data!="string"&&(i[0]|=128),t.enqueue(i),t.enqueue(n)})}})}let Ko;function qr(e){return e.reduce((t,n)=>t+n.length,0)}function Qr(e,t){if(e[0].length===t)return e.shift();const n=new Uint8Array(t);let r=0;for(let i=0;iMath.pow(2,53-32)-1){l.enqueue(Xs);break}i=c*Math.pow(2,32)+a.getUint32(4),r=3}else{if(qr(n)e){l.enqueue(Xs);break}}}})}const Ed=4;function Y(e){if(e)return qy(e)}function qy(e){for(var t in Y.prototype)e[t]=Y.prototype[t];return e}Y.prototype.on=Y.prototype.addEventListener=function(e,t){return this._callbacks=this._callbacks||{},(this._callbacks["$"+e]=this._callbacks["$"+e]||[]).push(t),this};Y.prototype.once=function(e,t){function n(){this.off(e,n),t.apply(this,arguments)}return n.fn=t,this.on(e,n),this};Y.prototype.off=Y.prototype.removeListener=Y.prototype.removeAllListeners=Y.prototype.removeEventListener=function(e,t){if(this._callbacks=this._callbacks||{},arguments.length==0)return this._callbacks={},this;var n=this._callbacks["$"+e];if(!n)return this;if(arguments.length==1)return delete this._callbacks["$"+e],this;for(var r,i=0;itypeof Promise=="function"&&typeof Promise.resolve=="function"?t=>Promise.resolve().then(t):(t,n)=>n(t,0))(),Oe=(()=>typeof self<"u"?self:typeof window<"u"?window:Function("return this")())(),Qy="arraybuffer";function kd(e,...t){return t.reduce((n,r)=>(e.hasOwnProperty(r)&&(n[r]=e[r]),n),{})}const Ky=Oe.setTimeout,Xy=Oe.clearTimeout;function fo(e,t){t.useNativeTimers?(e.setTimeoutFn=Ky.bind(Oe),e.clearTimeoutFn=Xy.bind(Oe)):(e.setTimeoutFn=Oe.setTimeout.bind(Oe),e.clearTimeoutFn=Oe.clearTimeout.bind(Oe))}const Yy=1.33;function Jy(e){return typeof e=="string"?Gy(e):Math.ceil((e.byteLength||e.size)*Yy)}function Gy(e){let t=0,n=0;for(let r=0,i=e.length;r=57344?n+=3:(r++,n+=4);return n}function _d(){return Date.now().toString(36).substring(3)+Math.random().toString(36).substring(2,5)}function Zy(e){let t="";for(let n in e)e.hasOwnProperty(n)&&(t.length&&(t+="&"),t+=encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t}function by(e){let t={},n=e.split("&");for(let r=0,i=n.length;r{this.readyState="paused",t()};if(this._polling||!this.writable){let r=0;this._polling&&(r++,this.once("pollComplete",function(){--r||n()})),this.writable||(r++,this.once("drain",function(){--r||n()}))}else n()}_poll(){this._polling=!0,this.doPoll(),this.emitReserved("poll")}onData(t){const n=r=>{if(this.readyState==="opening"&&r.type==="open"&&this.onOpen(),r.type==="close")return this.onClose({description:"transport closed by the server"}),!1;this.onPacket(r)};Vy(t,this.socket.binaryType).forEach(n),this.readyState!=="closed"&&(this._polling=!1,this.emitReserved("pollComplete"),this.readyState==="open"&&this._poll())}doClose(){const t=()=>{this.write([{type:"close"}])};this.readyState==="open"?t():this.once("open",t)}write(t){this.writable=!1,$y(t,n=>{this.doWrite(n,()=>{this.writable=!0,this.emitReserved("drain")})})}uri(){const t=this.opts.secure?"https":"http",n=this.query||{};return this.opts.timestampRequests!==!1&&(n[this.opts.timestampParam]=_d()),!this.supportsBinary&&!n.sid&&(n.b64=1),this.createUri(t,n)}}let xd=!1;try{xd=typeof XMLHttpRequest<"u"&&"withCredentials"in new XMLHttpRequest}catch{}const ng=xd;function rg(){}class ig extends tg{constructor(t){if(super(t),typeof location<"u"){const n=location.protocol==="https:";let r=location.port;r||(r=n?"443":"80"),this.xd=typeof location<"u"&&t.hostname!==location.hostname||r!==t.port}}doWrite(t,n){const r=this.request({method:"POST",data:t});r.on("success",n),r.on("error",(i,o)=>{this.onError("xhr post error",i,o)})}doPoll(){const t=this.request();t.on("data",this.onData.bind(this)),t.on("error",(n,r)=>{this.onError("xhr poll error",n,r)}),this.pollXhr=t}}let hn=class ci extends Y{constructor(t,n,r){super(),this.createRequest=t,fo(this,r),this._opts=r,this._method=r.method||"GET",this._uri=n,this._data=r.data!==void 0?r.data:null,this._create()}_create(){var t;const n=kd(this._opts,"agent","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");n.xdomain=!!this._opts.xd;const r=this._xhr=this.createRequest(n);try{r.open(this._method,this._uri,!0);try{if(this._opts.extraHeaders){r.setDisableHeaderCheck&&r.setDisableHeaderCheck(!0);for(let i in this._opts.extraHeaders)this._opts.extraHeaders.hasOwnProperty(i)&&r.setRequestHeader(i,this._opts.extraHeaders[i])}}catch{}if(this._method==="POST")try{r.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch{}try{r.setRequestHeader("Accept","*/*")}catch{}(t=this._opts.cookieJar)===null||t===void 0||t.addCookies(r),"withCredentials"in r&&(r.withCredentials=this._opts.withCredentials),this._opts.requestTimeout&&(r.timeout=this._opts.requestTimeout),r.onreadystatechange=()=>{var i;r.readyState===3&&((i=this._opts.cookieJar)===null||i===void 0||i.parseCookies(r.getResponseHeader("set-cookie"))),r.readyState===4&&(r.status===200||r.status===1223?this._onLoad():this.setTimeoutFn(()=>{this._onError(typeof r.status=="number"?r.status:0)},0))},r.send(this._data)}catch(i){this.setTimeoutFn(()=>{this._onError(i)},0);return}typeof document<"u"&&(this._index=ci.requestsCount++,ci.requests[this._index]=this)}_onError(t){this.emitReserved("error",t,this._xhr),this._cleanup(!0)}_cleanup(t){if(!(typeof this._xhr>"u"||this._xhr===null)){if(this._xhr.onreadystatechange=rg,t)try{this._xhr.abort()}catch{}typeof document<"u"&&delete ci.requests[this._index],this._xhr=null}}_onLoad(){const t=this._xhr.responseText;t!==null&&(this.emitReserved("data",t),this.emitReserved("success"),this._cleanup())}abort(){this._cleanup()}};hn.requestsCount=0;hn.requests={};if(typeof document<"u"){if(typeof attachEvent=="function")attachEvent("onunload",La);else if(typeof addEventListener=="function"){const e="onpagehide"in Oe?"pagehide":"unload";addEventListener(e,La,!1)}}function La(){for(let e in hn.requests)hn.requests.hasOwnProperty(e)&&hn.requests[e].abort()}const og=function(){const e=Cd({xdomain:!1});return e&&e.responseType!==null}();class sg extends ig{constructor(t){super(t);const n=t&&t.forceBase64;this.supportsBinary=og&&!n}request(t={}){return Object.assign(t,{xd:this.xd},this.opts),new hn(Cd,this.uri(),t)}}function Cd(e){const t=e.xdomain;try{if(typeof XMLHttpRequest<"u"&&(!t||ng))return new XMLHttpRequest}catch{}if(!t)try{return new Oe[["Active"].concat("Object").join("X")]("Microsoft.XMLHTTP")}catch{}}const Rd=typeof navigator<"u"&&typeof navigator.product=="string"&&navigator.product.toLowerCase()==="reactnative";class lg extends nu{get name(){return"websocket"}doOpen(){const t=this.uri(),n=this.opts.protocols,r=Rd?{}:kd(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(r.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,r)}catch(i){return this.emitReserved("error",i)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()}addEventListeners(){this.ws.onopen=()=>{this.opts.autoUnref&&this.ws._socket.unref(),this.onOpen()},this.ws.onclose=t=>this.onClose({description:"websocket connection closed",context:t}),this.ws.onmessage=t=>this.onData(t.data),this.ws.onerror=t=>this.onError("websocket error",t)}write(t){this.writable=!1;for(let n=0;n{try{this.doWrite(r,o)}catch{}i&&co(()=>{this.writable=!0,this.emitReserved("drain")},this.setTimeoutFn)})}}doClose(){typeof this.ws<"u"&&(this.ws.onerror=()=>{},this.ws.close(),this.ws=null)}uri(){const t=this.opts.secure?"wss":"ws",n=this.query||{};return this.opts.timestampRequests&&(n[this.opts.timestampParam]=_d()),this.supportsBinary||(n.b64=1),this.createUri(t,n)}}const Xo=Oe.WebSocket||Oe.MozWebSocket;class ug extends lg{createSocket(t,n,r){return Rd?new Xo(t,n,r):n?new Xo(t,n):new Xo(t)}doWrite(t,n){this.ws.send(n)}}class ag extends nu{get name(){return"webtransport"}doOpen(){try{this._transport=new WebTransport(this.createUri("https"),this.opts.transportOptions[this.name])}catch(t){return this.emitReserved("error",t)}this._transport.closed.then(()=>{this.onClose()}).catch(t=>{this.onError("webtransport error",t)}),this._transport.ready.then(()=>{this._transport.createBidirectionalStream().then(t=>{const n=Wy(Number.MAX_SAFE_INTEGER,this.socket.binaryType),r=t.readable.pipeThrough(n).getReader(),i=Hy();i.readable.pipeTo(t.writable),this._writer=i.writable.getWriter();const o=()=>{r.read().then(({done:l,value:u})=>{l||(this.onPacket(u),o())}).catch(l=>{})};o();const s={type:"open"};this.query.sid&&(s.data=`{"sid":"${this.query.sid}"}`),this._writer.write(s).then(()=>this.onOpen())})})}write(t){this.writable=!1;for(let n=0;n{i&&co(()=>{this.writable=!0,this.emitReserved("drain")},this.setTimeoutFn)})}}doClose(){var t;(t=this._transport)===null||t===void 0||t.close()}}const cg={websocket:ug,webtransport:ag,polling:sg},fg=/^(?:(?![^:@\/?#]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@\/?#]*)(?::([^:@\/?#]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,dg=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];function Ys(e){if(e.length>8e3)throw"URI too long";const t=e,n=e.indexOf("["),r=e.indexOf("]");n!=-1&&r!=-1&&(e=e.substring(0,n)+e.substring(n,r).replace(/:/g,";")+e.substring(r,e.length));let i=fg.exec(e||""),o={},s=14;for(;s--;)o[dg[s]]=i[s]||"";return n!=-1&&r!=-1&&(o.source=t,o.host=o.host.substring(1,o.host.length-1).replace(/;/g,":"),o.authority=o.authority.replace("[","").replace("]","").replace(/;/g,":"),o.ipv6uri=!0),o.pathNames=pg(o,o.path),o.queryKey=hg(o,o.query),o}function pg(e,t){const n=/\/{2,9}/g,r=t.replace(n,"/").split("/");return(t.slice(0,1)=="/"||t.length===0)&&r.splice(0,1),t.slice(-1)=="/"&&r.splice(r.length-1,1),r}function hg(e,t){const n={};return t.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,function(r,i,o){i&&(n[i]=o)}),n}const Js=typeof addEventListener=="function"&&typeof removeEventListener=="function",fi=[];Js&&addEventListener("offline",()=>{fi.forEach(e=>e())},!1);class xt extends Y{constructor(t,n){if(super(),this.binaryType=Qy,this.writeBuffer=[],this._prevBufferLen=0,this._pingInterval=-1,this._pingTimeout=-1,this._maxPayload=-1,this._pingTimeoutTime=1/0,t&&typeof t=="object"&&(n=t,t=null),t){const r=Ys(t);n.hostname=r.host,n.secure=r.protocol==="https"||r.protocol==="wss",n.port=r.port,r.query&&(n.query=r.query)}else n.host&&(n.hostname=Ys(n.host).host);fo(this,n),this.secure=n.secure!=null?n.secure:typeof location<"u"&&location.protocol==="https:",n.hostname&&!n.port&&(n.port=this.secure?"443":"80"),this.hostname=n.hostname||(typeof location<"u"?location.hostname:"localhost"),this.port=n.port||(typeof location<"u"&&location.port?location.port:this.secure?"443":"80"),this.transports=[],this._transportsByName={},n.transports.forEach(r=>{const i=r.prototype.name;this.transports.push(i),this._transportsByName[i]=r}),this.opts=Object.assign({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},n),this.opts.path=this.opts.path.replace(/\/$/,"")+(this.opts.addTrailingSlash?"/":""),typeof this.opts.query=="string"&&(this.opts.query=by(this.opts.query)),Js&&(this.opts.closeOnBeforeunload&&(this._beforeunloadEventListener=()=>{this.transport&&(this.transport.removeAllListeners(),this.transport.close())},addEventListener("beforeunload",this._beforeunloadEventListener,!1)),this.hostname!=="localhost"&&(this._offlineEventListener=()=>{this._onClose("transport close",{description:"network connection lost"})},fi.push(this._offlineEventListener))),this.opts.withCredentials&&(this._cookieJar=void 0),this._open()}createTransport(t){const n=Object.assign({},this.opts.query);n.EIO=Ed,n.transport=t,this.id&&(n.sid=this.id);const r=Object.assign({},this.opts,{query:n,socket:this,hostname:this.hostname,secure:this.secure,port:this.port},this.opts.transportOptions[t]);return new this._transportsByName[t](r)}_open(){if(this.transports.length===0){this.setTimeoutFn(()=>{this.emitReserved("error","No transports available")},0);return}const t=this.opts.rememberUpgrade&&xt.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1?"websocket":this.transports[0];this.readyState="opening";const n=this.createTransport(t);n.open(),this.setTransport(n)}setTransport(t){this.transport&&this.transport.removeAllListeners(),this.transport=t,t.on("drain",this._onDrain.bind(this)).on("packet",this._onPacket.bind(this)).on("error",this._onError.bind(this)).on("close",n=>this._onClose("transport close",n))}onOpen(){this.readyState="open",xt.priorWebsocketSuccess=this.transport.name==="websocket",this.emitReserved("open"),this.flush()}_onPacket(t){if(this.readyState==="opening"||this.readyState==="open"||this.readyState==="closing")switch(this.emitReserved("packet",t),this.emitReserved("heartbeat"),t.type){case"open":this.onHandshake(JSON.parse(t.data));break;case"ping":this._sendPacket("pong"),this.emitReserved("ping"),this.emitReserved("pong"),this._resetPingTimeout();break;case"error":const n=new Error("server error");n.code=t.data,this._onError(n);break;case"message":this.emitReserved("data",t.data),this.emitReserved("message",t.data);break}}onHandshake(t){this.emitReserved("handshake",t),this.id=t.sid,this.transport.query.sid=t.sid,this._pingInterval=t.pingInterval,this._pingTimeout=t.pingTimeout,this._maxPayload=t.maxPayload,this.onOpen(),this.readyState!=="closed"&&this._resetPingTimeout()}_resetPingTimeout(){this.clearTimeoutFn(this._pingTimeoutTimer);const t=this._pingInterval+this._pingTimeout;this._pingTimeoutTime=Date.now()+t,this._pingTimeoutTimer=this.setTimeoutFn(()=>{this._onClose("ping timeout")},t),this.opts.autoUnref&&this._pingTimeoutTimer.unref()}_onDrain(){this.writeBuffer.splice(0,this._prevBufferLen),this._prevBufferLen=0,this.writeBuffer.length===0?this.emitReserved("drain"):this.flush()}flush(){if(this.readyState!=="closed"&&this.transport.writable&&!this.upgrading&&this.writeBuffer.length){const t=this._getWritablePackets();this.transport.send(t),this._prevBufferLen=t.length,this.emitReserved("flush")}}_getWritablePackets(){if(!(this._maxPayload&&this.transport.name==="polling"&&this.writeBuffer.length>1))return this.writeBuffer;let n=1;for(let r=0;r0&&n>this._maxPayload)return this.writeBuffer.slice(0,r);n+=2}return this.writeBuffer}_hasPingExpired(){if(!this._pingTimeoutTime)return!0;const t=Date.now()>this._pingTimeoutTime;return t&&(this._pingTimeoutTime=0,co(()=>{this._onClose("ping timeout")},this.setTimeoutFn)),t}write(t,n,r){return this._sendPacket("message",t,n,r),this}send(t,n,r){return this._sendPacket("message",t,n,r),this}_sendPacket(t,n,r,i){if(typeof n=="function"&&(i=n,n=void 0),typeof r=="function"&&(i=r,r=null),this.readyState==="closing"||this.readyState==="closed")return;r=r||{},r.compress=r.compress!==!1;const o={type:t,data:n,options:r};this.emitReserved("packetCreate",o),this.writeBuffer.push(o),i&&this.once("flush",i),this.flush()}close(){const t=()=>{this._onClose("forced close"),this.transport.close()},n=()=>{this.off("upgrade",n),this.off("upgradeError",n),t()},r=()=>{this.once("upgrade",n),this.once("upgradeError",n)};return(this.readyState==="opening"||this.readyState==="open")&&(this.readyState="closing",this.writeBuffer.length?this.once("drain",()=>{this.upgrading?r():t()}):this.upgrading?r():t()),this}_onError(t){if(xt.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&this.readyState==="opening")return this.transports.shift(),this._open();this.emitReserved("error",t),this._onClose("transport error",t)}_onClose(t,n){if(this.readyState==="opening"||this.readyState==="open"||this.readyState==="closing"){if(this.clearTimeoutFn(this._pingTimeoutTimer),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),Js&&(this._beforeunloadEventListener&&removeEventListener("beforeunload",this._beforeunloadEventListener,!1),this._offlineEventListener)){const r=fi.indexOf(this._offlineEventListener);r!==-1&&fi.splice(r,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this._prevBufferLen=0}}}xt.protocol=Ed;class mg extends xt{constructor(){super(...arguments),this._upgrades=[]}onOpen(){if(super.onOpen(),this.readyState==="open"&&this.opts.upgrade)for(let t=0;t{r||(n.send([{type:"ping",data:"probe"}]),n.once("packet",p=>{if(!r)if(p.type==="pong"&&p.data==="probe"){if(this.upgrading=!0,this.emitReserved("upgrading",n),!n)return;xt.priorWebsocketSuccess=n.name==="websocket",this.transport.pause(()=>{r||this.readyState!=="closed"&&(c(),this.setTransport(n),n.send([{type:"upgrade"}]),this.emitReserved("upgrade",n),n=null,this.upgrading=!1,this.flush())})}else{const m=new Error("probe error");m.transport=n.name,this.emitReserved("upgradeError",m)}}))};function o(){r||(r=!0,c(),n.close(),n=null)}const s=p=>{const m=new Error("probe error: "+p);m.transport=n.name,o(),this.emitReserved("upgradeError",m)};function l(){s("transport closed")}function u(){s("socket closed")}function a(p){n&&p.name!==n.name&&o()}const c=()=>{n.removeListener("open",i),n.removeListener("error",s),n.removeListener("close",l),this.off("close",u),this.off("upgrading",a)};n.once("open",i),n.once("error",s),n.once("close",l),this.once("close",u),this.once("upgrading",a),this._upgrades.indexOf("webtransport")!==-1&&t!=="webtransport"?this.setTimeoutFn(()=>{r||n.open()},200):n.open()}onHandshake(t){this._upgrades=this._filterUpgrades(t.upgrades),super.onHandshake(t)}_filterUpgrades(t){const n=[];for(let r=0;rcg[i]).filter(i=>!!i)),super(t,r)}};function gg(e,t="",n){let r=e;n=n||typeof location<"u"&&location,e==null&&(e=n.protocol+"//"+n.host),typeof e=="string"&&(e.charAt(0)==="/"&&(e.charAt(1)==="/"?e=n.protocol+e:e=n.host+e),/^(https?|wss?):\/\//.test(e)||(typeof n<"u"?e=n.protocol+"//"+e:e="https://"+e),r=Ys(e)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";const o=r.host.indexOf(":")!==-1?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+o+":"+r.port+t,r.href=r.protocol+"://"+o+(n&&n.port===r.port?"":":"+r.port),r}const vg=typeof ArrayBuffer=="function",wg=e=>typeof ArrayBuffer.isView=="function"?ArrayBuffer.isView(e):e.buffer instanceof ArrayBuffer,Td=Object.prototype.toString,Sg=typeof Blob=="function"||typeof Blob<"u"&&Td.call(Blob)==="[object BlobConstructor]",Eg=typeof File=="function"||typeof File<"u"&&Td.call(File)==="[object FileConstructor]";function ru(e){return vg&&(e instanceof ArrayBuffer||wg(e))||Sg&&e instanceof Blob||Eg&&e instanceof File}function di(e,t){if(!e||typeof e!="object")return!1;if(Array.isArray(e)){for(let n=0,r=e.length;n=0&&e.num{delete this.acks[t];for(let l=0;l{this.io.clearTimeoutFn(o),n.apply(this,l)};s.withError=!0,this.acks[t]=s}emitWithAck(t,...n){return new Promise((r,i)=>{const o=(s,l)=>s?i(s):r(l);o.withError=!0,n.push(o),this.emit(t,...n)})}_addToQueue(t){let n;typeof t[t.length-1]=="function"&&(n=t.pop());const r={id:this._queueSeq++,tryCount:0,pending:!1,args:t,flags:Object.assign({fromQueue:!0},this.flags)};t.push((i,...o)=>r!==this._queue[0]?void 0:(i!==null?r.tryCount>this._opts.retries&&(this._queue.shift(),n&&n(i)):(this._queue.shift(),n&&n(null,...o)),r.pending=!1,this._drainQueue())),this._queue.push(r),this._drainQueue()}_drainQueue(t=!1){if(!this.connected||this._queue.length===0)return;const n=this._queue[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}packet(t){t.nsp=this.nsp,this.io._packet(t)}onopen(){typeof this.auth=="function"?this.auth(t=>{this._sendConnectPacket(t)}):this._sendConnectPacket(this.auth)}_sendConnectPacket(t){this.packet({type:z.CONNECT,data:this._pid?Object.assign({pid:this._pid,offset:this._lastOffset},t):t})}onerror(t){this.connected||this.emitReserved("connect_error",t)}onclose(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this._clearAcks()}_clearAcks(){Object.keys(this.acks).forEach(t=>{if(!this.sendBuffer.some(r=>String(r.id)===t)){const r=this.acks[t];delete this.acks[t],r.withError&&r.call(this,new Error("socket has been disconnected"))}})}onpacket(t){if(t.nsp===this.nsp)switch(t.type){case z.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case z.EVENT:case z.BINARY_EVENT:this.onevent(t);break;case z.ACK:case z.BINARY_ACK:this.onack(t);break;case z.DISCONNECT:this.ondisconnect();break;case z.CONNECT_ERROR:this.destroy();const r=new Error(t.data.message);r.data=t.data.data,this.emitReserved("connect_error",r);break}}onevent(t){const n=t.data||[];t.id!=null&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))}emitEvent(t){if(this._anyListeners&&this._anyListeners.length){const n=this._anyListeners.slice();for(const r of n)r.apply(this,t)}super.emit.apply(this,t),this._pid&&t.length&&typeof t[t.length-1]=="string"&&(this._lastOffset=t[t.length-1])}ack(t){const n=this;let r=!1;return function(...i){r||(r=!0,n.packet({type:z.ACK,id:t,data:i}))}}onack(t){const n=this.acks[t.id];typeof n=="function"&&(delete this.acks[t.id],n.withError&&t.data.unshift(null),n.apply(this,t.data))}onconnect(t,n){this.id=t,this.recovered=n&&this._pid===n,this._pid=n,this.connected=!0,this.emitBuffered(),this.emitReserved("connect"),this._drainQueue(!0)}emitBuffered(){this.receiveBuffer.forEach(t=>this.emitEvent(t)),this.receiveBuffer=[],this.sendBuffer.forEach(t=>{this.notifyOutgoingListeners(t),this.packet(t)}),this.sendBuffer=[]}ondisconnect(){this.destroy(),this.onclose("io server disconnect")}destroy(){this.subs&&(this.subs.forEach(t=>t()),this.subs=void 0),this.io._destroy(this)}disconnect(){return this.connected&&this.packet({type:z.DISCONNECT}),this.destroy(),this.connected&&this.onclose("io client disconnect"),this}close(){return this.disconnect()}compress(t){return this.flags.compress=t,this}get volatile(){return this.flags.volatile=!0,this}timeout(t){return this.flags.timeout=t,this}onAny(t){return this._anyListeners=this._anyListeners||[],this._anyListeners.push(t),this}prependAny(t){return this._anyListeners=this._anyListeners||[],this._anyListeners.unshift(t),this}offAny(t){if(!this._anyListeners)return this;if(t){const n=this._anyListeners;for(let r=0;r0&&e.jitter<=1?e.jitter:0,this.attempts=0}Tn.prototype.duration=function(){var e=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var t=Math.random(),n=Math.floor(t*this.jitter*e);e=Math.floor(t*10)&1?e+n:e-n}return Math.min(e,this.max)|0};Tn.prototype.reset=function(){this.attempts=0};Tn.prototype.setMin=function(e){this.ms=e};Tn.prototype.setMax=function(e){this.max=e};Tn.prototype.setJitter=function(e){this.jitter=e};class bs extends Y{constructor(t,n){var r;super(),this.nsps={},this.subs=[],t&&typeof t=="object"&&(n=t,t=void 0),n=n||{},n.path=n.path||"/socket.io",this.opts=n,fo(this,n),this.reconnection(n.reconnection!==!1),this.reconnectionAttempts(n.reconnectionAttempts||1/0),this.reconnectionDelay(n.reconnectionDelay||1e3),this.reconnectionDelayMax(n.reconnectionDelayMax||5e3),this.randomizationFactor((r=n.randomizationFactor)!==null&&r!==void 0?r:.5),this.backoff=new Tn({min:this.reconnectionDelay(),max:this.reconnectionDelayMax(),jitter:this.randomizationFactor()}),this.timeout(n.timeout==null?2e4:n.timeout),this._readyState="closed",this.uri=t;const i=n.parser||Ng;this.encoder=new i.Encoder,this.decoder=new i.Decoder,this._autoConnect=n.autoConnect!==!1,this._autoConnect&&this.open()}reconnection(t){return arguments.length?(this._reconnection=!!t,t||(this.skipReconnect=!0),this):this._reconnection}reconnectionAttempts(t){return t===void 0?this._reconnectionAttempts:(this._reconnectionAttempts=t,this)}reconnectionDelay(t){var n;return t===void 0?this._reconnectionDelay:(this._reconnectionDelay=t,(n=this.backoff)===null||n===void 0||n.setMin(t),this)}randomizationFactor(t){var n;return t===void 0?this._randomizationFactor:(this._randomizationFactor=t,(n=this.backoff)===null||n===void 0||n.setJitter(t),this)}reconnectionDelayMax(t){var n;return t===void 0?this._reconnectionDelayMax:(this._reconnectionDelayMax=t,(n=this.backoff)===null||n===void 0||n.setMax(t),this)}timeout(t){return arguments.length?(this._timeout=t,this):this._timeout}maybeReconnectOnOpen(){!this._reconnecting&&this._reconnection&&this.backoff.attempts===0&&this.reconnect()}open(t){if(~this._readyState.indexOf("open"))return this;this.engine=new yg(this.uri,this.opts);const n=this.engine,r=this;this._readyState="opening",this.skipReconnect=!1;const i=Ie(n,"open",function(){r.onopen(),t&&t()}),o=l=>{this.cleanup(),this._readyState="closed",this.emitReserved("error",l),t?t(l):this.maybeReconnectOnOpen()},s=Ie(n,"error",o);if(this._timeout!==!1){const l=this._timeout,u=this.setTimeoutFn(()=>{i(),o(new Error("timeout")),n.close()},l);this.opts.autoUnref&&u.unref(),this.subs.push(()=>{this.clearTimeoutFn(u)})}return this.subs.push(i),this.subs.push(s),this}connect(t){return this.open(t)}onopen(){this.cleanup(),this._readyState="open",this.emitReserved("open");const t=this.engine;this.subs.push(Ie(t,"ping",this.onping.bind(this)),Ie(t,"data",this.ondata.bind(this)),Ie(t,"error",this.onerror.bind(this)),Ie(t,"close",this.onclose.bind(this)),Ie(this.decoder,"decoded",this.ondecoded.bind(this)))}onping(){this.emitReserved("ping")}ondata(t){try{this.decoder.add(t)}catch(n){this.onclose("parse error",n)}}ondecoded(t){co(()=>{this.emitReserved("packet",t)},this.setTimeoutFn)}onerror(t){this.emitReserved("error",t)}socket(t,n){let r=this.nsps[t];return r?this._autoConnect&&!r.active&&r.connect():(r=new Nd(this,t,n),this.nsps[t]=r),r}_destroy(t){const n=Object.keys(this.nsps);for(const r of n)if(this.nsps[r].active)return;this._close()}_packet(t){const n=this.encoder.encode(t);for(let r=0;rt()),this.subs.length=0,this.decoder.destroy()}_close(){this.skipReconnect=!0,this._reconnecting=!1,this.onclose("forced close")}disconnect(){return this._close()}onclose(t,n){var r;this.cleanup(),(r=this.engine)===null||r===void 0||r.close(),this.backoff.reset(),this._readyState="closed",this.emitReserved("close",t,n),this._reconnection&&!this.skipReconnect&&this.reconnect()}reconnect(){if(this._reconnecting||this.skipReconnect)return this;const t=this;if(this.backoff.attempts>=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{const n=this.backoff.duration();this._reconnecting=!0;const r=this.setTimeoutFn(()=>{t.skipReconnect||(this.emitReserved("reconnect_attempt",t.backoff.attempts),!t.skipReconnect&&t.open(i=>{i?(t._reconnecting=!1,t.reconnect(),this.emitReserved("reconnect_error",i)):t.onreconnect()}))},n);this.opts.autoUnref&&r.unref(),this.subs.push(()=>{this.clearTimeoutFn(r)})}}onreconnect(){const t=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",t)}}const Mn={};function pi(e,t){typeof e=="object"&&(t=e,e=void 0),t=t||{};const n=gg(e,t.path||"/socket.io"),r=n.source,i=n.id,o=n.path,s=Mn[i]&&o in Mn[i].nsps,l=t.forceNew||t["force new connection"]||t.multiplex===!1||s;let u;return l?u=new bs(r,t):(Mn[i]||(Mn[i]=new bs(r,t)),u=Mn[i]),n.query&&!t.query&&(t.query=n.queryKey),u.socket(n.path,t)}Object.assign(pi,{Manager:bs,Socket:Nd,io:pi,connect:pi});function Pg(){const[e,t]=be.useState([]),[n,r]=be.useState(!0),[i,o]=be.useState(""),[s,l]=be.useState({});be.useEffect(()=>{const c=pi("/");return console.log("Socket connected"),c.on("redeployStatus",async({stackId:p,status:m})=>{if(l(S=>({...S,[p]:m})),!m)try{const g=(await qo.get("/api/stacks")).data.sort((v,k)=>v.Name.localeCompare(k.Name));t(g)}catch(S){console.error("Fehler beim Aktualisieren des Status nach Redeploy:",S)}}),()=>c.disconnect()},[]);const u=async()=>{try{const p=(await qo.get("/api/stacks")).data.sort((m,S)=>m.Name.localeCompare(S.Name));t(p)}catch(c){console.error("❌ Fehler beim Abrufen der Stacks:",c),o("Fehler beim Laden der Stacks")}finally{r(!1)}};be.useEffect(()=>{u()},[]);const a=async c=>{l(p=>({...p,[c]:!0}));try{await qo.put(`/api/stacks/${c}/redeploy`)}catch(p){console.error("❌ Fehler beim Redeploy:",p),l(m=>({...m,[c]:!1}))}};return n?G.jsx("p",{className:"text-gray-400",children:"Lade Stacks..."}):i?G.jsx("p",{className:"text-red-400",children:i}):G.jsxs("div",{className:"grid grid-cols-1 md:grid-cols-2 gap-6 p-6",children:[e.map(c=>{const p=s[c.Id]||!1,m=c.updateStatus==="✅";return G.jsxs("div",{className:`flex justify-between items-center p-5 rounded-xl shadow-lg transition + ${p?"bg-gray-700 cursor-not-allowed":"bg-gray-800 hover:bg-gray-700"}`,children:[G.jsxs("div",{className:"flex items-center space-x-4",children:[G.jsx("div",{className:`w-12 h-12 flex items-center justify-center rounded-full + ${c.updateStatus==="✅"?"bg-green-500":c.updateStatus==="⚠️"?"bg-yellow-500":"bg-red-500"}`}),G.jsxs("div",{children:[G.jsx("p",{className:"text-lg font-semibold text-white",children:c.Name}),G.jsxs("p",{className:"text-sm text-gray-400",children:["ID: ",c.Id]})]})]}),!m&&G.jsx("button",{onClick:()=>a(c.Id),disabled:p,className:`px-5 py-2 rounded-lg font-medium transition + ${p?"bg-orange-500 cursor-not-allowed":"bg-blue-500 hover:bg-blue-600"}`,children:p?"Redeploying":"Redeploy"})]},c.Id)}),e.length===0&&G.jsx("p",{className:"text-gray-400",children:"Keine Stacks gefunden."})]})}function Lg(){return G.jsxs("div",{className:"min-h-screen bg-gray-900 text-white",children:[G.jsxs("header",{className:"p-6 bg-gray-800 shadow-md",children:[G.jsx("h1",{className:"text-2xl font-bold text-white",children:"StackPulse"}),G.jsx("p",{className:"text-gray-400 mt-1",children:"Verwalte deine Docker Stacks"})]}),G.jsx("main",{className:"p-6",children:G.jsx(Pg,{})})]})}Yo.createRoot(document.getElementById("root")).render(G.jsx(Qd.StrictMode,{children:G.jsx(Lg,{})})); diff --git a/backend/public/index.html b/backend/public/index.html new file mode 100644 index 0000000..298053e --- /dev/null +++ b/backend/public/index.html @@ -0,0 +1,15 @@ + + + + + + StackPulse + + + + + +
+ + + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..f7ca830 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,11 @@ +version: '3.8' +services: + app: + build: + context: . + dockerfile: Dockerfile + ports: + - "${PORT}:${PORT}" + env_file: + - .env + restart: unless-stopped diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 9e4fe42..6a72de4 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -3,22 +3,31 @@ set -e echo "🚀 Starte StackPulse Dev-Umgebung..." +# --- Backend --- cd backend npm install npm start & BACK_PID=$! cd .. +# --- Frontend --- cd frontend npm install npm run dev & FRONT_PID=$! + +# Optional: Kopiere Build-Dateien für Backend /public (nur für statisches Testen) +npm run build +cp -r dist ../backend/public + cd .. echo "" echo "✅ StackPulse läuft lokal:" -echo "Frontend: http://localhost:5173" -echo "Backend: http://localhost:3300" +echo "Frontend (Vite Dev): http://localhost:5173" +echo "Backend API: http://localhost:3300" +echo "Frontend (statisch im Backend/public): http://localhost:4000" echo "Beenden mit STRG+C" +# Prozesse überwachen wait $BACK_PID $FRONT_PID diff --git a/scripts/start-dev.sh.bak b/scripts/start-dev.sh.bak new file mode 100755 index 0000000..9e4fe42 --- /dev/null +++ b/scripts/start-dev.sh.bak @@ -0,0 +1,24 @@ +#!/bin/bash +set -e + +echo "🚀 Starte StackPulse Dev-Umgebung..." + +cd backend +npm install +npm start & +BACK_PID=$! +cd .. + +cd frontend +npm install +npm run dev & +FRONT_PID=$! +cd .. + +echo "" +echo "✅ StackPulse läuft lokal:" +echo "Frontend: http://localhost:5173" +echo "Backend: http://localhost:3300" +echo "Beenden mit STRG+C" + +wait $BACK_PID $FRONT_PID -- 2.52.0 From 2a18c46a7d9c6eb856ec708650c5f4ab62808279 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 09:19:16 +0000 Subject: [PATCH 02/26] Update --- scripts/start-dev.sh.bak | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100755 scripts/start-dev.sh.bak diff --git a/scripts/start-dev.sh.bak b/scripts/start-dev.sh.bak deleted file mode 100755 index 9e4fe42..0000000 --- a/scripts/start-dev.sh.bak +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -set -e - -echo "🚀 Starte StackPulse Dev-Umgebung..." - -cd backend -npm install -npm start & -BACK_PID=$! -cd .. - -cd frontend -npm install -npm run dev & -FRONT_PID=$! -cd .. - -echo "" -echo "✅ StackPulse läuft lokal:" -echo "Frontend: http://localhost:5173" -echo "Backend: http://localhost:3300" -echo "Beenden mit STRG+C" - -wait $BACK_PID $FRONT_PID -- 2.52.0 From dc4dc63dddc12c0bb5fdce4a6b4ac794cdfb18b1 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 09:48:41 +0000 Subject: [PATCH 03/26] Update --- Dockerfile | 23 ----------------------- docker-compose.dev.yml | 11 ----------- scripts/switch-branch.sh | 11 ++++++++++- 3 files changed, 10 insertions(+), 35 deletions(-) delete mode 100644 Dockerfile delete mode 100644 docker-compose.dev.yml diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 9ba01f2..0000000 --- a/Dockerfile +++ /dev/null @@ -1,23 +0,0 @@ -# Stage 1: Frontend build -FROM node:20-alpine AS frontend-build -WORKDIR /app/frontend -COPY frontend/package.json frontend/package-lock.json ./ -RUN npm ci -COPY frontend/ ./ -RUN npm run build - -# Stage 2: Backend + static frontend -FROM node:20-alpine AS runtime -WORKDIR /app/backend -COPY backend/package.json backend/package-lock.json ./ -RUN npm ci --only=production -COPY backend/ ./ -COPY --from=frontend-build /app/frontend/build ./public - -ENV NODE_ENV=production -EXPOSE 4000 # Port wird über .env gesetzt - -RUN chown -R node:node /app/backend -USER node - -CMD ["node", "index.js"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index f7ca830..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,11 +0,0 @@ -version: '3.8' -services: - app: - build: - context: . - dockerfile: Dockerfile - ports: - - "${PORT}:${PORT}" - env_file: - - .env - restart: unless-stopped diff --git a/scripts/switch-branch.sh b/scripts/switch-branch.sh index 71e2fcf..c025b64 100755 --- a/scripts/switch-branch.sh +++ b/scripts/switch-branch.sh @@ -44,9 +44,18 @@ fi SELECTED_BRANCH=${BRANCH_MAP[$choice]} echo "Wechsle zu Branch: $SELECTED_BRANCH" -# Wechseln, ggf. Branch erstellen, wenn nur Remote existiert +# Remote-Stand holen +git fetch origin + +# Branch wechseln (lokal erstellen, falls nur remote vorhanden) if git show-ref --verify --quiet refs/heads/$SELECTED_BRANCH; then git checkout $SELECTED_BRANCH else git checkout -b $SELECTED_BRANCH origin/$SELECTED_BRANCH fi + +# Arbeitsverzeichnis exakt auf Remote-Branch zurücksetzen +git reset --hard origin/$SELECTED_BRANCH +git clean -fd + +echo "Branch '$SELECTED_BRANCH' ist nun aktiv. Arbeitsverzeichnis entspricht exakt dem Remote-Stand." -- 2.52.0 From 2accfef46564dfbff79b6668e017bb07c06ac412 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 09:53:01 +0000 Subject: [PATCH 04/26] Update --- frontend/index.html.bak | 12 ------------ frontend/tailwind.config.js.bak | 10 ---------- 2 files changed, 22 deletions(-) delete mode 100644 frontend/index.html.bak delete mode 100644 frontend/tailwind.config.js.bak diff --git a/frontend/index.html.bak b/frontend/index.html.bak deleted file mode 100644 index 6591fa7..0000000 --- a/frontend/index.html.bak +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - StackPulse - - -
- - - diff --git a/frontend/tailwind.config.js.bak b/frontend/tailwind.config.js.bak deleted file mode 100644 index 3718949..0000000 --- a/frontend/tailwind.config.js.bak +++ /dev/null @@ -1,10 +0,0 @@ -export default { - content: [ - "./index.html", - "./src/**/*.{js,ts,jsx,tsx}", - ], - theme: { - extend: {}, - }, - plugins: [], -} -- 2.52.0 From 16a932ec9062c555c29b3ebf3843cad357e1fba3 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 10:03:09 +0000 Subject: [PATCH 05/26] Update --- backend/index.js | 2 +- frontend/src/App.jsx.bak | 11 --- frontend/src/Stacks.jsx.bak | 112 ---------------------------- frontend/src/Stacks.jsx.bak_working | 112 ---------------------------- frontend/src/index.css.bak | 3 - frontend/src/main.jsx.bak | 10 --- frontend/vite.config.js | 2 +- 7 files changed, 2 insertions(+), 250 deletions(-) delete mode 100644 frontend/src/App.jsx.bak delete mode 100644 frontend/src/Stacks.jsx.bak delete mode 100644 frontend/src/Stacks.jsx.bak_working delete mode 100644 frontend/src/index.css.bak delete mode 100644 frontend/src/main.jsx.bak diff --git a/backend/index.js b/backend/index.js index a0f0b65..52d0094 100644 --- a/backend/index.js +++ b/backend/index.js @@ -10,7 +10,7 @@ dotenv.config(); const app = express(); app.use(express.json()); -const PORT = process.env.PORT || 4000; +const PORT = 4000; // HTTPS Agent für Self-Signed-Zertifikate const agent = new https.Agent({ rejectUnauthorized: false }); diff --git a/frontend/src/App.jsx.bak b/frontend/src/App.jsx.bak deleted file mode 100644 index 4484844..0000000 --- a/frontend/src/App.jsx.bak +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react"; -import Stacks from "./Stacks.jsx"; - -export default function App() { - return ( -
-

StackPulse

- -
- ); -} diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak deleted file mode 100644 index ddd32be..0000000 --- a/frontend/src/Stacks.jsx.bak +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect, useState } from "react"; -import axios from "axios"; -import { io } from "socket.io-client"; - -export default function Stacks() { - const [stacks, setStacks] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); // { stackId: true/false } - - // WebSocket initialisieren - useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); - - socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); - - // Wenn Redeploy beendet, Stack-Status neu laden - if (!status) { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); - } - } - }); - - return () => socket.disconnect(); - }, []); - - // Stacks initial laden - const fetchStacks = async () => { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("❌ Fehler beim Abrufen der Stacks:", err); - setError("Fehler beim Laden der Stacks"); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchStacks(); - }, []); - - // Redeploy eines Stacks - const handleRedeploy = async (stackId) => { - // Button sofort auf Redeploying setzen - setRedeploying(prev => ({ ...prev, [stackId]: true })); - - try { - await axios.put(`/api/stacks/${stackId}/redeploy`); - } catch (err) { - console.error("❌ Fehler beim Redeploy:", err); - } - - // Stack-Status immer neu laden - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren nach Redeploy:", err); - } finally { - // Erst nach Datenaktualisierung Button zurücksetzen - setRedeploying(prev => ({ ...prev, [stackId]: false })); - } - }; - - if (loading) return

Lade Stacks...

; - if (error) return

{error}

; - - return ( -
    - {stacks.map(stack => { - const isRedeploying = redeploying[stack.Id] || false; - return ( -
  • -
    -
    {stack.updateStatus}
    -
    -

    {stack.Name}

    -

    ID: {stack.Id}

    -
    -
    - -
  • - ); - })} - {stacks.length === 0 &&

    Keine Stacks gefunden.

    } -
- ); -} diff --git a/frontend/src/Stacks.jsx.bak_working b/frontend/src/Stacks.jsx.bak_working deleted file mode 100644 index ddd32be..0000000 --- a/frontend/src/Stacks.jsx.bak_working +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect, useState } from "react"; -import axios from "axios"; -import { io } from "socket.io-client"; - -export default function Stacks() { - const [stacks, setStacks] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); // { stackId: true/false } - - // WebSocket initialisieren - useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); - - socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); - - // Wenn Redeploy beendet, Stack-Status neu laden - if (!status) { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); - } - } - }); - - return () => socket.disconnect(); - }, []); - - // Stacks initial laden - const fetchStacks = async () => { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("❌ Fehler beim Abrufen der Stacks:", err); - setError("Fehler beim Laden der Stacks"); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchStacks(); - }, []); - - // Redeploy eines Stacks - const handleRedeploy = async (stackId) => { - // Button sofort auf Redeploying setzen - setRedeploying(prev => ({ ...prev, [stackId]: true })); - - try { - await axios.put(`/api/stacks/${stackId}/redeploy`); - } catch (err) { - console.error("❌ Fehler beim Redeploy:", err); - } - - // Stack-Status immer neu laden - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren nach Redeploy:", err); - } finally { - // Erst nach Datenaktualisierung Button zurücksetzen - setRedeploying(prev => ({ ...prev, [stackId]: false })); - } - }; - - if (loading) return

Lade Stacks...

; - if (error) return

{error}

; - - return ( -
    - {stacks.map(stack => { - const isRedeploying = redeploying[stack.Id] || false; - return ( -
  • -
    -
    {stack.updateStatus}
    -
    -

    {stack.Name}

    -

    ID: {stack.Id}

    -
    -
    - -
  • - ); - })} - {stacks.length === 0 &&

    Keine Stacks gefunden.

    } -
- ); -} diff --git a/frontend/src/index.css.bak b/frontend/src/index.css.bak deleted file mode 100644 index b5c61c9..0000000 --- a/frontend/src/index.css.bak +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/frontend/src/main.jsx.bak b/frontend/src/main.jsx.bak deleted file mode 100644 index b194025..0000000 --- a/frontend/src/main.jsx.bak +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App.jsx"; -import './index.css'; // Tailwind CSS - -ReactDOM.createRoot(document.getElementById("root")).render( - - - -); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index bc8b97d..67cdfd0 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,7 +8,7 @@ export default defineConfig({ port: 5173, proxy: { "/api": { - target: "http://127.0.0.1:3300", // dein Backend + target: "http://127.0.0.1:4000", // dein Backend changeOrigin: true, }, }, -- 2.52.0 From 20a1a4c841e23da94dbf2f0b32f2d9e6acec65fa Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 10:04:28 +0000 Subject: [PATCH 06/26] Update --- scripts/start-dev.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 6a72de4..4361402 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -25,8 +25,7 @@ cd .. echo "" echo "✅ StackPulse läuft lokal:" echo "Frontend (Vite Dev): http://localhost:5173" -echo "Backend API: http://localhost:3300" -echo "Frontend (statisch im Backend/public): http://localhost:4000" +echo "Backend API: http://localhost:4000" echo "Beenden mit STRG+C" # Prozesse überwachen -- 2.52.0 From 97611f8ef4fb34a81cdb1475927c760befeb51dc Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 10:41:53 +0000 Subject: [PATCH 07/26] Update --- backend/index.js | 57 ++++++++--------- backend/index.js.bak | 144 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+), 33 deletions(-) create mode 100644 backend/index.js.bak diff --git a/backend/index.js b/backend/index.js index 52d0094..76450b1 100644 --- a/backend/index.js +++ b/backend/index.js @@ -4,18 +4,32 @@ import https from 'https'; import axios from 'axios'; import http from 'http'; import { Server } from 'socket.io'; +import path from 'path'; +import { fileURLToPath } from 'url'; dotenv.config(); +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + const app = express(); app.use(express.json()); -const PORT = 4000; +// Statische Dateien ausliefern (Frontend Build) +app.use(express.static(path.join(__dirname, 'public'))); + +// SPA Fallback für React-Router +app.get('*', (req, res, next) => { + if (req.path.startsWith('/api')) return next(); // API Requests weiterleiten + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +const PORT = process.env.PORT || 5173; // HTTPS Agent für Self-Signed-Zertifikate const agent = new https.Agent({ rejectUnauthorized: false }); -// Axios-Instance für alle Portainer-Requests +// Axios-Instance für Portainer-Requests const axiosInstance = axios.create({ httpsAgent: agent, headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, @@ -28,28 +42,21 @@ const redeployingStacks = {}; // { [stackId]: true/false } // HTTP Server + Socket.IO const server = http.createServer(app); const io = new Server(server, { - cors: { origin: "*" } // ggf. auf Frontend-URL anpassen + cors: { origin: "*" } }); -// Socket.IO Verbindung io.on("connection", (socket) => { console.log("Client verbunden:", socket.id); }); -// Hilfsfunktion zum Broadcasten des Redeploy-Status const broadcastRedeployStatus = (stackId, status) => { redeployingStacks[stackId] = status; io.emit("redeployStatus", { stackId, status }); console.log(`Stack ${stackId} redeploying: ${status}`); }; -// Root-Endpoint -app.get('/', (req, res) => { - console.log("Root Endpoint aufgerufen"); - res.send('StackPulse Backend läuft. Nutze /api/stacks für die Daten.'); -}); +// --- API Endpoints --- -// Alle Stacks abrufen app.get('/api/stacks', async (req, res) => { try { const stacksRes = await axiosInstance.get('/api/stacks'); @@ -58,21 +65,11 @@ app.get('/api/stacks', async (req, res) => { stacksRes.data.map(async (stack) => { try { const statusRes = await axiosInstance.get(`/api/stacks/${stack.Id}/images_status?refresh=true`); - let statusEmoji = '✅'; // up-to-date - if (statusRes.data.Status === 'outdated') statusEmoji = '⚠️'; // outdated - - return { - ...stack, - updateStatus: statusEmoji, - redeploying: redeployingStacks[stack.Id] || false, - }; + let statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; + return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; } catch (err) { - console.error(`Fehler beim Abrufen Remote Digest für Stack ${stack.Id}:`, err.message); - return { - ...stack, - updateStatus: '❌', // Fehler - redeploying: redeployingStacks[stack.Id] || false, - }; + console.error(`Fehler Remote Digest Stack ${stack.Id}:`, err.message); + return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); @@ -86,12 +83,10 @@ app.get('/api/stacks', async (req, res) => { } }); -// Redeploy eines Stacks app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; try { - // Status auf "redeploying" setzen & an alle Clients senden broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); @@ -102,7 +97,6 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } else if (stack.Type === 2) { const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; - if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); const services = fileRes.data?.Config?.services || {}; @@ -114,7 +108,7 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); } catch (pullErr) { - console.error(`Fehler beim Pull von ${imageName}:`, pullErr.message); + console.error(`Fehler Pull ${imageName}:`, pullErr.message); } } @@ -125,14 +119,11 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { ); } - // Redeploy beendet, Status an alle Clients senden broadcastRedeployStatus(id, false); - res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { - // Fehler → Status zurücksetzen & an Clients senden broadcastRedeployStatus(id, false); - console.error(`Fehler beim Redeploy von Stack ${id}:`, err.message); + console.error(`Fehler Redeploy Stack ${id}:`, err.message); if (err.response) res.status(err.response.status).json(err.response.data); else res.status(500).json({ error: err.message }); } diff --git a/backend/index.js.bak b/backend/index.js.bak new file mode 100644 index 0000000..52d0094 --- /dev/null +++ b/backend/index.js.bak @@ -0,0 +1,144 @@ +import express from 'express'; +import dotenv from 'dotenv'; +import https from 'https'; +import axios from 'axios'; +import http from 'http'; +import { Server } from 'socket.io'; + +dotenv.config(); + +const app = express(); +app.use(express.json()); + +const PORT = 4000; + +// HTTPS Agent für Self-Signed-Zertifikate +const agent = new https.Agent({ rejectUnauthorized: false }); + +// Axios-Instance für alle Portainer-Requests +const axiosInstance = axios.create({ + httpsAgent: agent, + headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, + baseURL: process.env.PORTAINER_URL, +}); + +// In-Memory Store für Redeploy-Status +const redeployingStacks = {}; // { [stackId]: true/false } + +// HTTP Server + Socket.IO +const server = http.createServer(app); +const io = new Server(server, { + cors: { origin: "*" } // ggf. auf Frontend-URL anpassen +}); + +// Socket.IO Verbindung +io.on("connection", (socket) => { + console.log("Client verbunden:", socket.id); +}); + +// Hilfsfunktion zum Broadcasten des Redeploy-Status +const broadcastRedeployStatus = (stackId, status) => { + redeployingStacks[stackId] = status; + io.emit("redeployStatus", { stackId, status }); + console.log(`Stack ${stackId} redeploying: ${status}`); +}; + +// Root-Endpoint +app.get('/', (req, res) => { + console.log("Root Endpoint aufgerufen"); + res.send('StackPulse Backend läuft. Nutze /api/stacks für die Daten.'); +}); + +// Alle Stacks abrufen +app.get('/api/stacks', async (req, res) => { + try { + const stacksRes = await axiosInstance.get('/api/stacks'); + + const stacksWithStatus = await Promise.all( + stacksRes.data.map(async (stack) => { + try { + const statusRes = await axiosInstance.get(`/api/stacks/${stack.Id}/images_status?refresh=true`); + let statusEmoji = '✅'; // up-to-date + if (statusRes.data.Status === 'outdated') statusEmoji = '⚠️'; // outdated + + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false, + }; + } catch (err) { + console.error(`Fehler beim Abrufen Remote Digest für Stack ${stack.Id}:`, err.message); + return { + ...stack, + updateStatus: '❌', // Fehler + redeploying: redeployingStacks[stack.Id] || false, + }; + } + }) + ); + + stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + res.json(stacksWithStatus); + } catch (err) { + console.error('Fehler beim Abrufen der Stacks:', err.message); + if (err.response) res.status(err.response.status).json(err.response.data); + else res.status(500).json({ error: err.message }); + } +}); + +// Redeploy eines Stacks +app.put('/api/stacks/:id/redeploy', async (req, res) => { + const { id } = req.params; + + try { + // Status auf "redeploying" setzen & an alle Clients senden + broadcastRedeployStatus(id, true); + + const stackRes = await axiosInstance.get(`/api/stacks/${id}`); + const stack = stackRes.data; + + if (stack.Type === 1) { + await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); + } else if (stack.Type === 2) { + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); + const stackFileContent = fileRes.data?.StackFileContent; + + if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); + + const services = fileRes.data?.Config?.services || {}; + for (const serviceName in services) { + const imageName = services[serviceName].image; + if (!imageName) continue; + try { + await axiosInstance.post( + `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` + ); + } catch (pullErr) { + console.error(`Fehler beim Pull von ${imageName}:`, pullErr.message); + } + } + + await axiosInstance.put( + `/api/stacks/${id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } + ); + } + + // Redeploy beendet, Status an alle Clients senden + broadcastRedeployStatus(id, false); + + res.json({ success: true, message: 'Stack redeployed' }); + } catch (err) { + // Fehler → Status zurücksetzen & an Clients senden + broadcastRedeployStatus(id, false); + console.error(`Fehler beim Redeploy von Stack ${id}:`, err.message); + if (err.response) res.status(err.response.status).json(err.response.data); + else res.status(500).json({ error: err.message }); + } +}); + +// Server starten +server.listen(PORT, '0.0.0.0', () => { + console.log(`Backend läuft auf Port ${PORT}`); +}); -- 2.52.0 From 80d5e47ac859c0280358e97f67d08f949aefdf4f Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 12:18:06 +0000 Subject: [PATCH 08/26] Update --- backend/index.js | 46 +++++-------- backend/index.js.bak | 144 ---------------------------------------- frontend/vite.config.js | 9 +-- scripts/start-dev.sh | 2 +- 4 files changed, 20 insertions(+), 181 deletions(-) delete mode 100644 backend/index.js.bak diff --git a/backend/index.js b/backend/index.js index 76450b1..ede24fc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -15,35 +15,34 @@ const __dirname = path.dirname(__filename); const app = express(); app.use(express.json()); -// Statische Dateien ausliefern (Frontend Build) +// Statische Frontend-Dateien ausliefern app.use(express.static(path.join(__dirname, 'public'))); -// SPA Fallback für React-Router +// SPA-Fallback für React-Router app.get('*', (req, res, next) => { - if (req.path.startsWith('/api')) return next(); // API Requests weiterleiten + if (req.path.startsWith('/api')) return next(); res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -const PORT = process.env.PORT || 5173; +// Backend-Port fix +const PORT = 4001; // HTTPS Agent für Self-Signed-Zertifikate const agent = new https.Agent({ rejectUnauthorized: false }); -// Axios-Instance für Portainer-Requests +// Axios-Instance für Portainer const axiosInstance = axios.create({ httpsAgent: agent, headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, baseURL: process.env.PORTAINER_URL, }); -// In-Memory Store für Redeploy-Status -const redeployingStacks = {}; // { [stackId]: true/false } +// In-Memory Redeploy-Status +const redeployingStacks = {}; // HTTP Server + Socket.IO const server = http.createServer(app); -const io = new Server(server, { - cors: { origin: "*" } -}); +const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { console.log("Client verbunden:", socket.id); @@ -52,40 +51,34 @@ io.on("connection", (socket) => { const broadcastRedeployStatus = (stackId, status) => { redeployingStacks[stackId] = status; io.emit("redeployStatus", { stackId, status }); - console.log(`Stack ${stackId} redeploying: ${status}`); }; // --- API Endpoints --- - app.get('/api/stacks', async (req, res) => { try { const stacksRes = await axiosInstance.get('/api/stacks'); - const stacksWithStatus = await Promise.all( stacksRes.data.map(async (stack) => { try { - const statusRes = await axiosInstance.get(`/api/stacks/${stack.Id}/images_status?refresh=true`); + const statusRes = await axiosInstance.get( + `/api/stacks/${stack.Id}/images_status?refresh=true` + ); let statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; - } catch (err) { - console.error(`Fehler Remote Digest Stack ${stack.Id}:`, err.message); + } catch { return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); - stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); res.json(stacksWithStatus); } catch (err) { - console.error('Fehler beim Abrufen der Stacks:', err.message); - if (err.response) res.status(err.response.status).json(err.response.data); - else res.status(500).json({ error: err.message }); + res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; - try { broadcastRedeployStatus(id, true); @@ -107,13 +100,10 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch (pullErr) { - console.error(`Fehler Pull ${imageName}:`, pullErr.message); - } + } catch {} } - await axiosInstance.put( - `/api/stacks/${id}`, + await axiosInstance.put(`/api/stacks/${id}`, { StackFileContent: stackFileContent, Prune: false, PullImage: true }, { params: { endpointId: stack.EndpointId } } ); @@ -123,9 +113,7 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { broadcastRedeployStatus(id, false); - console.error(`Fehler Redeploy Stack ${id}:`, err.message); - if (err.response) res.status(err.response.status).json(err.response.data); - else res.status(500).json({ error: err.message }); + res.status(500).json({ error: err.message }); } }); diff --git a/backend/index.js.bak b/backend/index.js.bak deleted file mode 100644 index 52d0094..0000000 --- a/backend/index.js.bak +++ /dev/null @@ -1,144 +0,0 @@ -import express from 'express'; -import dotenv from 'dotenv'; -import https from 'https'; -import axios from 'axios'; -import http from 'http'; -import { Server } from 'socket.io'; - -dotenv.config(); - -const app = express(); -app.use(express.json()); - -const PORT = 4000; - -// HTTPS Agent für Self-Signed-Zertifikate -const agent = new https.Agent({ rejectUnauthorized: false }); - -// Axios-Instance für alle Portainer-Requests -const axiosInstance = axios.create({ - httpsAgent: agent, - headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, - baseURL: process.env.PORTAINER_URL, -}); - -// In-Memory Store für Redeploy-Status -const redeployingStacks = {}; // { [stackId]: true/false } - -// HTTP Server + Socket.IO -const server = http.createServer(app); -const io = new Server(server, { - cors: { origin: "*" } // ggf. auf Frontend-URL anpassen -}); - -// Socket.IO Verbindung -io.on("connection", (socket) => { - console.log("Client verbunden:", socket.id); -}); - -// Hilfsfunktion zum Broadcasten des Redeploy-Status -const broadcastRedeployStatus = (stackId, status) => { - redeployingStacks[stackId] = status; - io.emit("redeployStatus", { stackId, status }); - console.log(`Stack ${stackId} redeploying: ${status}`); -}; - -// Root-Endpoint -app.get('/', (req, res) => { - console.log("Root Endpoint aufgerufen"); - res.send('StackPulse Backend läuft. Nutze /api/stacks für die Daten.'); -}); - -// Alle Stacks abrufen -app.get('/api/stacks', async (req, res) => { - try { - const stacksRes = await axiosInstance.get('/api/stacks'); - - const stacksWithStatus = await Promise.all( - stacksRes.data.map(async (stack) => { - try { - const statusRes = await axiosInstance.get(`/api/stacks/${stack.Id}/images_status?refresh=true`); - let statusEmoji = '✅'; // up-to-date - if (statusRes.data.Status === 'outdated') statusEmoji = '⚠️'; // outdated - - return { - ...stack, - updateStatus: statusEmoji, - redeploying: redeployingStacks[stack.Id] || false, - }; - } catch (err) { - console.error(`Fehler beim Abrufen Remote Digest für Stack ${stack.Id}:`, err.message); - return { - ...stack, - updateStatus: '❌', // Fehler - redeploying: redeployingStacks[stack.Id] || false, - }; - } - }) - ); - - stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); - res.json(stacksWithStatus); - } catch (err) { - console.error('Fehler beim Abrufen der Stacks:', err.message); - if (err.response) res.status(err.response.status).json(err.response.data); - else res.status(500).json({ error: err.message }); - } -}); - -// Redeploy eines Stacks -app.put('/api/stacks/:id/redeploy', async (req, res) => { - const { id } = req.params; - - try { - // Status auf "redeploying" setzen & an alle Clients senden - broadcastRedeployStatus(id, true); - - const stackRes = await axiosInstance.get(`/api/stacks/${id}`); - const stack = stackRes.data; - - if (stack.Type === 1) { - await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); - } else if (stack.Type === 2) { - const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); - const stackFileContent = fileRes.data?.StackFileContent; - - if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); - - const services = fileRes.data?.Config?.services || {}; - for (const serviceName in services) { - const imageName = services[serviceName].image; - if (!imageName) continue; - try { - await axiosInstance.post( - `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` - ); - } catch (pullErr) { - console.error(`Fehler beim Pull von ${imageName}:`, pullErr.message); - } - } - - await axiosInstance.put( - `/api/stacks/${id}`, - { StackFileContent: stackFileContent, Prune: false, PullImage: true }, - { params: { endpointId: stack.EndpointId } } - ); - } - - // Redeploy beendet, Status an alle Clients senden - broadcastRedeployStatus(id, false); - - res.json({ success: true, message: 'Stack redeployed' }); - } catch (err) { - // Fehler → Status zurücksetzen & an Clients senden - broadcastRedeployStatus(id, false); - console.error(`Fehler beim Redeploy von Stack ${id}:`, err.message); - if (err.response) res.status(err.response.status).json(err.response.data); - else res.status(500).json({ error: err.message }); - } -}); - -// Server starten -server.listen(PORT, '0.0.0.0', () => { - console.log(`Backend läuft auf Port ${PORT}`); -}); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 67cdfd0..18ba8c0 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,15 +8,10 @@ export default defineConfig({ port: 5173, proxy: { "/api": { - target: "http://127.0.0.1:4000", // dein Backend + target: "http://localhost:4001", // dein Backend changeOrigin: true, }, }, - allowedHosts: [ - "10.10.10.23", // dein Dev-Rechner - "stackpulse.d-razz.de", // der Host, den du brauchst - "localhost", - ], + allowedHosts: "all", }, }); - diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 4361402..8a59a51 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -25,7 +25,7 @@ cd .. echo "" echo "✅ StackPulse läuft lokal:" echo "Frontend (Vite Dev): http://localhost:5173" -echo "Backend API: http://localhost:4000" +echo "Backend API: http://localhost:4001" echo "Beenden mit STRG+C" # Prozesse überwachen -- 2.52.0 From 9db80682067c15c69643874193c8e0317b759c9b Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 13:25:25 +0000 Subject: [PATCH 09/26] Final commit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index deed335..01f3642 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ dist/ .env +scripts/docker-release.sh -- 2.52.0 From c4b93b5288a90612bcf4b95b0744cbe7cc380582 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 13:56:36 +0000 Subject: [PATCH 10/26] Update --- backend/index.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/backend/index.js b/backend/index.js index ede24fc..dcc837e 100644 --- a/backend/index.js +++ b/backend/index.js @@ -27,6 +27,9 @@ app.get('*', (req, res, next) => { // Backend-Port fix const PORT = 4001; +// Endpoint-ID aus der env +const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); + // HTTPS Agent für Self-Signed-Zertifikate const agent = new https.Agent({ rejectUnauthorized: false }); @@ -57,19 +60,24 @@ const broadcastRedeployStatus = (stackId, status) => { app.get('/api/stacks', async (req, res) => { try { const stacksRes = await axiosInstance.get('/api/stacks'); + + // Filter nach Endpoint-ID + const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + const stacksWithStatus = await Promise.all( - stacksRes.data.map(async (stack) => { + filteredStacks.map(async (stack) => { try { const statusRes = await axiosInstance.get( `/api/stacks/${stack.Id}/images_status?refresh=true` ); - let statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; + const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; } catch { return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); + stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); res.json(stacksWithStatus); } catch (err) { @@ -85,6 +93,11 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const stackRes = await axiosInstance.get(`/api/stacks/${id}`); const stack = stackRes.data; + // Prüfen, ob Stack zum konfigurierten Endpoint gehört + if (stack.EndpointId !== ENDPOINT_ID) { + throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); + } + if (stack.Type === 1) { await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { -- 2.52.0 From 10fa988bed756b186f7e22476798257d496d2ed5 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 14:11:02 +0000 Subject: [PATCH 11/26] Update --- frontend/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 18ba8c0..aa3ae76 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -8,7 +8,7 @@ export default defineConfig({ port: 5173, proxy: { "/api": { - target: "http://localhost:4001", // dein Backend + target: "http://127.0.0.1:4001", // dein Backend changeOrigin: true, }, }, -- 2.52.0 From c3e670d9b70a6e5abf2512cf841d956dcc805756 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 14:34:29 +0000 Subject: [PATCH 12/26] Update --- scripts/start-dev.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 8a59a51..8caf75d 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -18,7 +18,7 @@ FRONT_PID=$! # Optional: Kopiere Build-Dateien für Backend /public (nur für statisches Testen) npm run build -cp -r dist ../backend/public +#cp -r dist ../backend/public cd .. -- 2.52.0 From 2e93449b55e715c37263c613a0d42f93e5366661 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:03:33 +0000 Subject: [PATCH 13/26] Update --- scripts/switch-branch.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/switch-branch.sh b/scripts/switch-branch.sh index c025b64..0249a19 100755 --- a/scripts/switch-branch.sh +++ b/scripts/switch-branch.sh @@ -1,5 +1,19 @@ #!/bin/bash +# =============================== +# Dateien sichern, die nicht gepusht werden +# =============================== +UNVERSIONED_FILES=("scripts/docker-release.sh") +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f $f ]]; then + mkdir -p /tmp/git_safe_backup + cp "$f" "/tmp/git_safe_backup/$(basename "$f")" + fi +done + +# =============================== +# Alle Branches sammeln +# =============================== # Alle lokalen Branches holen, führende Sternchen und Leerzeichen entfernen LOCAL_BRANCHES=$(git branch | sed 's/* //' | sed 's/^[[:space:]]*//') @@ -58,4 +72,16 @@ fi git reset --hard origin/$SELECTED_BRANCH git clean -fd +# =============================== +# Gesicherte unversionierte Dateien zurückkopieren +# =============================== +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f "/tmp/git_safe_backup/$(basename "$f")" ]]; then + mkdir -p "$(dirname "$f")" + mv "/tmp/git_safe_backup/$(basename "$f")" "$f" + fi +done +rm -rf /tmp/git_safe_backup + echo "Branch '$SELECTED_BRANCH' ist nun aktiv. Arbeitsverzeichnis entspricht exakt dem Remote-Stand." +echo "Unversionierte Dateien wurden wiederhergestellt." -- 2.52.0 From e4b48d70f1ca9178eaa66103354c120e97dceb15 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:04:06 +0000 Subject: [PATCH 14/26] Update --- scripts/switch-branch.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/switch-branch.sh b/scripts/switch-branch.sh index c025b64..0249a19 100755 --- a/scripts/switch-branch.sh +++ b/scripts/switch-branch.sh @@ -1,5 +1,19 @@ #!/bin/bash +# =============================== +# Dateien sichern, die nicht gepusht werden +# =============================== +UNVERSIONED_FILES=("scripts/docker-release.sh") +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f $f ]]; then + mkdir -p /tmp/git_safe_backup + cp "$f" "/tmp/git_safe_backup/$(basename "$f")" + fi +done + +# =============================== +# Alle Branches sammeln +# =============================== # Alle lokalen Branches holen, führende Sternchen und Leerzeichen entfernen LOCAL_BRANCHES=$(git branch | sed 's/* //' | sed 's/^[[:space:]]*//') @@ -58,4 +72,16 @@ fi git reset --hard origin/$SELECTED_BRANCH git clean -fd +# =============================== +# Gesicherte unversionierte Dateien zurückkopieren +# =============================== +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f "/tmp/git_safe_backup/$(basename "$f")" ]]; then + mkdir -p "$(dirname "$f")" + mv "/tmp/git_safe_backup/$(basename "$f")" "$f" + fi +done +rm -rf /tmp/git_safe_backup + echo "Branch '$SELECTED_BRANCH' ist nun aktiv. Arbeitsverzeichnis entspricht exakt dem Remote-Stand." +echo "Unversionierte Dateien wurden wiederhergestellt." -- 2.52.0 From 1b9fa894056b233b85ac75a96c3c4d3d431fd793 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:05:45 +0000 Subject: [PATCH 15/26] Update --- scripts/switch-branch.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/scripts/switch-branch.sh b/scripts/switch-branch.sh index c025b64..0249a19 100755 --- a/scripts/switch-branch.sh +++ b/scripts/switch-branch.sh @@ -1,5 +1,19 @@ #!/bin/bash +# =============================== +# Dateien sichern, die nicht gepusht werden +# =============================== +UNVERSIONED_FILES=("scripts/docker-release.sh") +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f $f ]]; then + mkdir -p /tmp/git_safe_backup + cp "$f" "/tmp/git_safe_backup/$(basename "$f")" + fi +done + +# =============================== +# Alle Branches sammeln +# =============================== # Alle lokalen Branches holen, führende Sternchen und Leerzeichen entfernen LOCAL_BRANCHES=$(git branch | sed 's/* //' | sed 's/^[[:space:]]*//') @@ -58,4 +72,16 @@ fi git reset --hard origin/$SELECTED_BRANCH git clean -fd +# =============================== +# Gesicherte unversionierte Dateien zurückkopieren +# =============================== +for f in "${UNVERSIONED_FILES[@]}"; do + if [[ -f "/tmp/git_safe_backup/$(basename "$f")" ]]; then + mkdir -p "$(dirname "$f")" + mv "/tmp/git_safe_backup/$(basename "$f")" "$f" + fi +done +rm -rf /tmp/git_safe_backup + echo "Branch '$SELECTED_BRANCH' ist nun aktiv. Arbeitsverzeichnis entspricht exakt dem Remote-Stand." +echo "Unversionierte Dateien wurden wiederhergestellt." -- 2.52.0 From 2008ccc7338d502459af6fb84d22ee41c6fd873a Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:09:02 +0000 Subject: [PATCH 16/26] Update --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 00fa812..2691cf2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,6 +8,7 @@ npm-debug.log .idea frontend/build frontend/dist +backend/.env backend/node_modules frontend/node_modules -- 2.52.0 From 51e022ae52451672cd8d2891b3d96c7cfd51009c Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:31:31 +0000 Subject: [PATCH 17/26] Update --- backend/index.js | 11 +++++++- scripts/docker-release.sh | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 scripts/docker-release.sh diff --git a/backend/index.js b/backend/index.js index dcc837e..1a26dfc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -64,8 +64,17 @@ app.get('/api/stacks', async (req, res) => { // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + // Deduplication nach Name: nur einmal pro Name + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) { + uniqueStacksMap[stack.Name] = stack; + } + }); + const uniqueStacks = Object.values(uniqueStacksMap); + const stacksWithStatus = await Promise.all( - filteredStacks.map(async (stack) => { + uniqueStacks.map(async (stack) => { try { const statusRes = await axiosInstance.get( `/api/stacks/${stack.Id}/images_status?refresh=true` diff --git a/scripts/docker-release.sh b/scripts/docker-release.sh new file mode 100644 index 0000000..c079f8e --- /dev/null +++ b/scripts/docker-release.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -e + +# --- Konfiguration --- +GHCR_USERNAME="" +REPO_NAME="" + +# --- Branch prüfen --- +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [[ "$BRANCH" != "master" ]]; then + echo "Fehler: Du musst auf 'master' sein, um ein Release zu machen." + exit 1 +fi + +# --- Versionsnummer abfragen --- +while true; do + read -p "Bitte Versionsnummer für das Release eingeben (z.B. v0.2.0): " VERSION_TAG + if [[ -n "$VERSION_TAG" ]]; then break; else echo "Versionsnummer darf nicht leer sein."; fi +done + +# --- Git: Pull und Merge sicherstellen --- +git pull origin master + +# --- Git: Commit ausstehender Änderungen --- +git add . +read -p "Commit-Nachricht eingeben (default: 'Release $VERSION_TAG'): " COMMIT_MSG +COMMIT_MSG=${COMMIT_MSG:-"Release $VERSION_TAG"} +git commit -m "$COMMIT_MSG" || echo "Keine Änderungen zum Committen." + +# --- Git: Tag setzen --- +if git rev-parse "$VERSION_TAG" >/dev/null 2>&1; then + echo "Tag $VERSION_TAG existiert bereits. Bitte zuerst löschen oder neuen Tag wählen." + exit 1 +fi + +git tag -a "$VERSION_TAG" -m "Release $VERSION_TAG" + +# --- Git: Push master + Tag --- +git push origin master +git push origin "$VERSION_TAG" +echo "Tag $VERSION_TAG gesetzt und auf master gepusht." + +# --- Docker: Login --- +if [ -z "$CR_PAT" ]; then + echo "CR_PAT (GitHub Token) nicht gesetzt! Bitte export CR_PAT=" + exit 1 +fi +echo $CR_PAT | docker login ghcr.io -u $GHCR_USERNAME --password-stdin + +# --- Docker: Build & Tag --- +docker build -t ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG . +docker tag ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest + +# --- Docker: Push --- +docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG +docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest + +echo "Release $VERSION_TAG erfolgreich auf master + GHCR gepusht!" -- 2.52.0 From d7c43567aade04a6bde148a304e0681b05827fd2 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:31:53 +0000 Subject: [PATCH 18/26] Update --- backend/index.js | 11 +++++++- scripts/docker-release.sh | 58 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 scripts/docker-release.sh diff --git a/backend/index.js b/backend/index.js index dcc837e..1a26dfc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -64,8 +64,17 @@ app.get('/api/stacks', async (req, res) => { // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + // Deduplication nach Name: nur einmal pro Name + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) { + uniqueStacksMap[stack.Name] = stack; + } + }); + const uniqueStacks = Object.values(uniqueStacksMap); + const stacksWithStatus = await Promise.all( - filteredStacks.map(async (stack) => { + uniqueStacks.map(async (stack) => { try { const statusRes = await axiosInstance.get( `/api/stacks/${stack.Id}/images_status?refresh=true` diff --git a/scripts/docker-release.sh b/scripts/docker-release.sh new file mode 100644 index 0000000..c079f8e --- /dev/null +++ b/scripts/docker-release.sh @@ -0,0 +1,58 @@ +#!/bin/bash +set -e + +# --- Konfiguration --- +GHCR_USERNAME="" +REPO_NAME="" + +# --- Branch prüfen --- +BRANCH=$(git rev-parse --abbrev-ref HEAD) +if [[ "$BRANCH" != "master" ]]; then + echo "Fehler: Du musst auf 'master' sein, um ein Release zu machen." + exit 1 +fi + +# --- Versionsnummer abfragen --- +while true; do + read -p "Bitte Versionsnummer für das Release eingeben (z.B. v0.2.0): " VERSION_TAG + if [[ -n "$VERSION_TAG" ]]; then break; else echo "Versionsnummer darf nicht leer sein."; fi +done + +# --- Git: Pull und Merge sicherstellen --- +git pull origin master + +# --- Git: Commit ausstehender Änderungen --- +git add . +read -p "Commit-Nachricht eingeben (default: 'Release $VERSION_TAG'): " COMMIT_MSG +COMMIT_MSG=${COMMIT_MSG:-"Release $VERSION_TAG"} +git commit -m "$COMMIT_MSG" || echo "Keine Änderungen zum Committen." + +# --- Git: Tag setzen --- +if git rev-parse "$VERSION_TAG" >/dev/null 2>&1; then + echo "Tag $VERSION_TAG existiert bereits. Bitte zuerst löschen oder neuen Tag wählen." + exit 1 +fi + +git tag -a "$VERSION_TAG" -m "Release $VERSION_TAG" + +# --- Git: Push master + Tag --- +git push origin master +git push origin "$VERSION_TAG" +echo "Tag $VERSION_TAG gesetzt und auf master gepusht." + +# --- Docker: Login --- +if [ -z "$CR_PAT" ]; then + echo "CR_PAT (GitHub Token) nicht gesetzt! Bitte export CR_PAT=" + exit 1 +fi +echo $CR_PAT | docker login ghcr.io -u $GHCR_USERNAME --password-stdin + +# --- Docker: Build & Tag --- +docker build -t ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG . +docker tag ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest + +# --- Docker: Push --- +docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG +docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest + +echo "Release $VERSION_TAG erfolgreich auf master + GHCR gepusht!" -- 2.52.0 From 6cf882da14d42f58d7ff33cac0d07df2c3f9b5a4 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 24 Sep 2025 15:46:21 +0000 Subject: [PATCH 19/26] Update --- backend/index.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backend/index.js b/backend/index.js index dcc837e..1a26dfc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -64,8 +64,17 @@ app.get('/api/stacks', async (req, res) => { // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + // Deduplication nach Name: nur einmal pro Name + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) { + uniqueStacksMap[stack.Name] = stack; + } + }); + const uniqueStacks = Object.values(uniqueStacksMap); + const stacksWithStatus = await Promise.all( - filteredStacks.map(async (stack) => { + uniqueStacks.map(async (stack) => { try { const statusRes = await axiosInstance.get( `/api/stacks/${stack.Id}/images_status?refresh=true` -- 2.52.0 From 716cd13c53d3a5e268fd260633a1fb338066ab02 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 06:37:44 +0000 Subject: [PATCH 20/26] Update --- frontend/src/Stacks.jsx | 79 ++++++++++++++++++-------- frontend/src/Stacks.jsx.bak | 109 ++++++++++++++++++++++++++++++++++++ scripts/docker-release.sh | 30 ++-------- 3 files changed, 169 insertions(+), 49 deletions(-) create mode 100644 frontend/src/Stacks.jsx.bak mode change 100644 => 100755 scripts/docker-release.sh diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 9560ebd..8b7bf27 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,37 +6,59 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); + // Map: { [stackId]: boolean } const [redeploying, setRedeploying] = useState({}); - // WebSocket initialisieren + // ------------------------- + // WebSocket: connect + events + // ------------------------- useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); + // wenn Frontend und Backend auf verschiedenen Hosts/Ports laufen: + // const socket = io("http://localhost:4001"); + const socket = io(); // gleiche Origin (Express static) -> passt normalerweise + socket.on("connect", () => console.log("Socket connected:", socket.id)); socket.on("redeployStatus", async ({ stackId, status }) => { + // sofort den lokalen map-state setzen (optimistisch) setRedeploying(prev => ({ ...prev, [stackId]: status })); - // Wenn Redeploy beendet, Stack-Status neu laden + // Wenn der Redeploy zu false geht, holen wir die aktuellen Stacks (aktualisieren UI) if (!status) { try { const res = await axios.get("/api/stacks"); const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); + + // Map aus API-Daten aufbauen (wichtig für F5 / Konsistenz) + const map = {}; + sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); + setRedeploying(map); } catch (err) { console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); } } }); - return () => socket.disconnect(); + return () => { + socket.off("redeployStatus"); + socket.disconnect(); + }; }, []); - // Stacks initial laden + // ------------------------- + // Initiale Stacks laden (auch setzt redeploying map) + // ------------------------- const fetchStacks = async () => { + setLoading(true); try { const res = await axios.get("/api/stacks"); const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); + + // Wichtig: redeploying-Map aus API-Daten setzen, damit F5 den echten Zustand zeigt + const map = {}; + sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); + setRedeploying(map); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -49,60 +71,71 @@ export default function Stacks() { fetchStacks(); }, []); - // Redeploy eines Stacks + // ------------------------- + // Redeploy Trigger + // ------------------------- const handleRedeploy = async (stackId) => { + // sofort UI-Feedback setRedeploying(prev => ({ ...prev, [stackId]: true })); try { await axios.put(`/api/stacks/${stackId}/redeploy`); + // kein sofortiges setRedeploying(false) – Backend sendet das finale Event } catch (err) { console.error("❌ Fehler beim Redeploy:", err); + // Fehlerfall: wieder auf false setzen damit UI nicht hängen bleibt setRedeploying(prev => ({ ...prev, [stackId]: false })); } }; + // ------------------------- + // Render + // ------------------------- if (loading) return

Lade Stacks...

; if (error) return

{error}

; return (
{stacks.map(stack => { - const isRedeploying = redeploying[stack.Id] || false; - const isUpToDate = stack.updateStatus === "✅"; + const isRedeploying = Boolean(redeploying[stack.Id]); return (
"gedämpfter" Container (sichtbar deaktiviert) className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition - ${isRedeploying ? "bg-gray-700 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`} + ${isRedeploying ? "bg-gray-700 opacity-60" : "bg-gray-800 hover:bg-gray-700"}`} + aria-disabled={isRedeploying} >
- {/* Status Indicator */}
-
+ />

{stack.Name}

ID: {stack.Id}

- {!isUpToDate && ( - - )} + {/* Button ist jetzt immer sichtbar */} +
); })} + {stacks.length === 0 &&

Keine Stacks gefunden.

}
); diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak new file mode 100644 index 0000000..9560ebd --- /dev/null +++ b/frontend/src/Stacks.jsx.bak @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { io } from "socket.io-client"; + +export default function Stacks() { + const [stacks, setStacks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [redeploying, setRedeploying] = useState({}); + + // WebSocket initialisieren + useEffect(() => { + const socket = io("/"); + console.log("Socket connected"); + + socket.on("redeployStatus", async ({ stackId, status }) => { + setRedeploying(prev => ({ ...prev, [stackId]: status })); + + // Wenn Redeploy beendet, Stack-Status neu laden + if (!status) { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + } + } + }); + + return () => socket.disconnect(); + }, []); + + // Stacks initial laden + const fetchStacks = async () => { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("❌ Fehler beim Abrufen der Stacks:", err); + setError("Fehler beim Laden der Stacks"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStacks(); + }, []); + + // Redeploy eines Stacks + const handleRedeploy = async (stackId) => { + setRedeploying(prev => ({ ...prev, [stackId]: true })); + + try { + await axios.put(`/api/stacks/${stackId}/redeploy`); + } catch (err) { + console.error("❌ Fehler beim Redeploy:", err); + setRedeploying(prev => ({ ...prev, [stackId]: false })); + } + }; + + if (loading) return

Lade Stacks...

; + if (error) return

{error}

; + + return ( +
+ {stacks.map(stack => { + const isRedeploying = redeploying[stack.Id] || false; + const isUpToDate = stack.updateStatus === "✅"; + + return ( +
+
+ {/* Status Indicator */} +
+
+
+

{stack.Name}

+

ID: {stack.Id}

+
+
+ + {!isUpToDate && ( + + )} +
+ ); + })} + {stacks.length === 0 &&

Keine Stacks gefunden.

} +
+ ); +} diff --git a/scripts/docker-release.sh b/scripts/docker-release.sh old mode 100644 new mode 100755 index c079f8e..d584e68 --- a/scripts/docker-release.sh +++ b/scripts/docker-release.sh @@ -2,8 +2,8 @@ set -e # --- Konfiguration --- -GHCR_USERNAME="" -REPO_NAME="" +GHCR_USERNAME="mboehmlaender" +REPO_NAME="stackpulse" # --- Branch prüfen --- BRANCH=$(git rev-parse --abbrev-ref HEAD) @@ -14,32 +14,10 @@ fi # --- Versionsnummer abfragen --- while true; do - read -p "Bitte Versionsnummer für das Release eingeben (z.B. v0.2.0): " VERSION_TAG + read -p "Bitte Versionsnummer für das Docker-Image eingeben (z.B. v0.1): " VERSION_TAG if [[ -n "$VERSION_TAG" ]]; then break; else echo "Versionsnummer darf nicht leer sein."; fi done -# --- Git: Pull und Merge sicherstellen --- -git pull origin master - -# --- Git: Commit ausstehender Änderungen --- -git add . -read -p "Commit-Nachricht eingeben (default: 'Release $VERSION_TAG'): " COMMIT_MSG -COMMIT_MSG=${COMMIT_MSG:-"Release $VERSION_TAG"} -git commit -m "$COMMIT_MSG" || echo "Keine Änderungen zum Committen." - -# --- Git: Tag setzen --- -if git rev-parse "$VERSION_TAG" >/dev/null 2>&1; then - echo "Tag $VERSION_TAG existiert bereits. Bitte zuerst löschen oder neuen Tag wählen." - exit 1 -fi - -git tag -a "$VERSION_TAG" -m "Release $VERSION_TAG" - -# --- Git: Push master + Tag --- -git push origin master -git push origin "$VERSION_TAG" -echo "Tag $VERSION_TAG gesetzt und auf master gepusht." - # --- Docker: Login --- if [ -z "$CR_PAT" ]; then echo "CR_PAT (GitHub Token) nicht gesetzt! Bitte export CR_PAT=" @@ -55,4 +33,4 @@ docker tag ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG ghcr.io/$GHCR_USERNAME docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest -echo "Release $VERSION_TAG erfolgreich auf master + GHCR gepusht!" +echo "Docker-Release $VERSION_TAG erfolgreich gebaut und zu GHCR gepusht!" -- 2.52.0 From 9beb812bceecb0f3683d3548d58fcae962227f33 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 06:39:14 +0000 Subject: [PATCH 21/26] Update --- scripts/docker-release.sh | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) mode change 100644 => 100755 scripts/docker-release.sh diff --git a/scripts/docker-release.sh b/scripts/docker-release.sh old mode 100644 new mode 100755 index c079f8e..d584e68 --- a/scripts/docker-release.sh +++ b/scripts/docker-release.sh @@ -2,8 +2,8 @@ set -e # --- Konfiguration --- -GHCR_USERNAME="" -REPO_NAME="" +GHCR_USERNAME="mboehmlaender" +REPO_NAME="stackpulse" # --- Branch prüfen --- BRANCH=$(git rev-parse --abbrev-ref HEAD) @@ -14,32 +14,10 @@ fi # --- Versionsnummer abfragen --- while true; do - read -p "Bitte Versionsnummer für das Release eingeben (z.B. v0.2.0): " VERSION_TAG + read -p "Bitte Versionsnummer für das Docker-Image eingeben (z.B. v0.1): " VERSION_TAG if [[ -n "$VERSION_TAG" ]]; then break; else echo "Versionsnummer darf nicht leer sein."; fi done -# --- Git: Pull und Merge sicherstellen --- -git pull origin master - -# --- Git: Commit ausstehender Änderungen --- -git add . -read -p "Commit-Nachricht eingeben (default: 'Release $VERSION_TAG'): " COMMIT_MSG -COMMIT_MSG=${COMMIT_MSG:-"Release $VERSION_TAG"} -git commit -m "$COMMIT_MSG" || echo "Keine Änderungen zum Committen." - -# --- Git: Tag setzen --- -if git rev-parse "$VERSION_TAG" >/dev/null 2>&1; then - echo "Tag $VERSION_TAG existiert bereits. Bitte zuerst löschen oder neuen Tag wählen." - exit 1 -fi - -git tag -a "$VERSION_TAG" -m "Release $VERSION_TAG" - -# --- Git: Push master + Tag --- -git push origin master -git push origin "$VERSION_TAG" -echo "Tag $VERSION_TAG gesetzt und auf master gepusht." - # --- Docker: Login --- if [ -z "$CR_PAT" ]; then echo "CR_PAT (GitHub Token) nicht gesetzt! Bitte export CR_PAT=" @@ -55,4 +33,4 @@ docker tag ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG ghcr.io/$GHCR_USERNAME docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:$VERSION_TAG docker push ghcr.io/$GHCR_USERNAME/$REPO_NAME:latest -echo "Release $VERSION_TAG erfolgreich auf master + GHCR gepusht!" +echo "Docker-Release $VERSION_TAG erfolgreich gebaut und zu GHCR gepusht!" -- 2.52.0 From 9ecc7c9ee2622df31210423b11e38ed2868a6ee8 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 06:41:20 +0000 Subject: [PATCH 22/26] Update --- frontend/src/Stacks.jsx.bak | 0 frontend/src/Stacks.jsx.bak_working | 112 ---------------------------- 2 files changed, 112 deletions(-) create mode 100644 frontend/src/Stacks.jsx.bak delete mode 100644 frontend/src/Stacks.jsx.bak_working diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/Stacks.jsx.bak_working b/frontend/src/Stacks.jsx.bak_working deleted file mode 100644 index ddd32be..0000000 --- a/frontend/src/Stacks.jsx.bak_working +++ /dev/null @@ -1,112 +0,0 @@ -import React, { useEffect, useState } from "react"; -import axios from "axios"; -import { io } from "socket.io-client"; - -export default function Stacks() { - const [stacks, setStacks] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); // { stackId: true/false } - - // WebSocket initialisieren - useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); - - socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); - - // Wenn Redeploy beendet, Stack-Status neu laden - if (!status) { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); - } - } - }); - - return () => socket.disconnect(); - }, []); - - // Stacks initial laden - const fetchStacks = async () => { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("❌ Fehler beim Abrufen der Stacks:", err); - setError("Fehler beim Laden der Stacks"); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchStacks(); - }, []); - - // Redeploy eines Stacks - const handleRedeploy = async (stackId) => { - // Button sofort auf Redeploying setzen - setRedeploying(prev => ({ ...prev, [stackId]: true })); - - try { - await axios.put(`/api/stacks/${stackId}/redeploy`); - } catch (err) { - console.error("❌ Fehler beim Redeploy:", err); - } - - // Stack-Status immer neu laden - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - } catch (err) { - console.error("Fehler beim Aktualisieren nach Redeploy:", err); - } finally { - // Erst nach Datenaktualisierung Button zurücksetzen - setRedeploying(prev => ({ ...prev, [stackId]: false })); - } - }; - - if (loading) return

Lade Stacks...

; - if (error) return

{error}

; - - return ( -
    - {stacks.map(stack => { - const isRedeploying = redeploying[stack.Id] || false; - return ( -
  • -
    -
    {stack.updateStatus}
    -
    -

    {stack.Name}

    -

    ID: {stack.Id}

    -
    -
    - -
  • - ); - })} - {stacks.length === 0 &&

    Keine Stacks gefunden.

    } -
- ); -} -- 2.52.0 From 5c20088088c08fa0308669d0a053090282a18219 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 06:47:50 +0000 Subject: [PATCH 23/26] Update --- frontend/src/Stacks.jsx.bak | 109 ++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak index e69de29..9560ebd 100644 --- a/frontend/src/Stacks.jsx.bak +++ b/frontend/src/Stacks.jsx.bak @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { io } from "socket.io-client"; + +export default function Stacks() { + const [stacks, setStacks] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [redeploying, setRedeploying] = useState({}); + + // WebSocket initialisieren + useEffect(() => { + const socket = io("/"); + console.log("Socket connected"); + + socket.on("redeployStatus", async ({ stackId, status }) => { + setRedeploying(prev => ({ ...prev, [stackId]: status })); + + // Wenn Redeploy beendet, Stack-Status neu laden + if (!status) { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + } + } + }); + + return () => socket.disconnect(); + }, []); + + // Stacks initial laden + const fetchStacks = async () => { + try { + const res = await axios.get("/api/stacks"); + const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); + setStacks(sortedStacks); + } catch (err) { + console.error("❌ Fehler beim Abrufen der Stacks:", err); + setError("Fehler beim Laden der Stacks"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStacks(); + }, []); + + // Redeploy eines Stacks + const handleRedeploy = async (stackId) => { + setRedeploying(prev => ({ ...prev, [stackId]: true })); + + try { + await axios.put(`/api/stacks/${stackId}/redeploy`); + } catch (err) { + console.error("❌ Fehler beim Redeploy:", err); + setRedeploying(prev => ({ ...prev, [stackId]: false })); + } + }; + + if (loading) return

Lade Stacks...

; + if (error) return

{error}

; + + return ( +
+ {stacks.map(stack => { + const isRedeploying = redeploying[stack.Id] || false; + const isUpToDate = stack.updateStatus === "✅"; + + return ( +
+
+ {/* Status Indicator */} +
+
+
+

{stack.Name}

+

ID: {stack.Id}

+
+
+ + {!isUpToDate && ( + + )} +
+ ); + })} + {stacks.length === 0 &&

Keine Stacks gefunden.

} +
+ ); +} -- 2.52.0 From 6c702b971c37185aff971eaa67f32e42b785e16f Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 07:00:39 +0000 Subject: [PATCH 24/26] Update --- backend/index.js | 28 +++++-- backend/index.js.bak | 145 ++++++++++++++++++++++++++++++++++++ frontend/src/Stacks.jsx | 28 +++---- frontend/src/Stacks.jsx.bak | 79 ++++++++++++++------ 4 files changed, 232 insertions(+), 48 deletions(-) create mode 100644 backend/index.js.bak diff --git a/backend/index.js b/backend/index.js index 1a26dfc..c9f6f97 100644 --- a/backend/index.js +++ b/backend/index.js @@ -48,7 +48,7 @@ const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("Client verbunden:", socket.id); + console.log("🔌 Client verbunden:", socket.id); }); const broadcastRedeployStatus = (stackId, status) => { @@ -59,12 +59,13 @@ const broadcastRedeployStatus = (stackId, status) => { // --- API Endpoints --- app.get('/api/stacks', async (req, res) => { try { + console.log("📦 Stacks werden von Portainer abgefragt …"); const stacksRes = await axiosInstance.get('/api/stacks'); - + // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); - // Deduplication nach Name: nur einmal pro Name + // Deduplication nach Name const uniqueStacksMap = {}; filteredStacks.forEach(stack => { if (!uniqueStacksMap[stack.Name]) { @@ -90,13 +91,17 @@ app.get('/api/stacks', async (req, res) => { stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); res.json(stacksWithStatus); } catch (err) { + console.error("❌ Fehler beim Laden der Stacks:", err.message); res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; + const startTime = Date.now(); + try { + console.log(`🟢 Redeploy gestartet für Stack ${id} …`); broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); @@ -108,8 +113,11 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } if (stack.Type === 1) { + console.log(`📂 Git-Stack erkannt (${stack.Name}) → Git-Redeploy`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { + console.log(`🐳 Docker-Compose-Stack erkannt (${stack.Name}) → Images pullen & neu deployen`); + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); @@ -119,21 +127,29 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { + console.log(`⬇️ Pulling image: ${imageName}`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch {} + } catch (err) { + console.warn(`⚠️ Konnte Image ${imageName} nicht pullen:`, err.message); + } } - await axiosInstance.put(`/api/stacks/${id}`, + await axiosInstance.put( + `/api/stacks/${id}`, { StackFileContent: stackFileContent, Prune: false, PullImage: true }, { params: { endpointId: stack.EndpointId } } ); } + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(`✅ Redeploy erfolgreich abgeschlossen für Stack ${id} (${stack.Name}) in ${duration}s`); + broadcastRedeployStatus(id, false); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { + console.error(`❌ Redeploy-Fehler für Stack ${id}:`, err.message); broadcastRedeployStatus(id, false); res.status(500).json({ error: err.message }); } @@ -141,5 +157,5 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { // Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`Backend läuft auf Port ${PORT}`); + console.log(`🚀 Backend läuft auf Port ${PORT}`); }); diff --git a/backend/index.js.bak b/backend/index.js.bak new file mode 100644 index 0000000..1a26dfc --- /dev/null +++ b/backend/index.js.bak @@ -0,0 +1,145 @@ +import express from 'express'; +import dotenv from 'dotenv'; +import https from 'https'; +import axios from 'axios'; +import http from 'http'; +import { Server } from 'socket.io'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const app = express(); +app.use(express.json()); + +// Statische Frontend-Dateien ausliefern +app.use(express.static(path.join(__dirname, 'public'))); + +// SPA-Fallback für React-Router +app.get('*', (req, res, next) => { + if (req.path.startsWith('/api')) return next(); + res.sendFile(path.join(__dirname, 'public', 'index.html')); +}); + +// Backend-Port fix +const PORT = 4001; + +// Endpoint-ID aus der env +const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); + +// HTTPS Agent für Self-Signed-Zertifikate +const agent = new https.Agent({ rejectUnauthorized: false }); + +// Axios-Instance für Portainer +const axiosInstance = axios.create({ + httpsAgent: agent, + headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, + baseURL: process.env.PORTAINER_URL, +}); + +// In-Memory Redeploy-Status +const redeployingStacks = {}; + +// HTTP Server + Socket.IO +const server = http.createServer(app); +const io = new Server(server, { cors: { origin: "*" } }); + +io.on("connection", (socket) => { + console.log("Client verbunden:", socket.id); +}); + +const broadcastRedeployStatus = (stackId, status) => { + redeployingStacks[stackId] = status; + io.emit("redeployStatus", { stackId, status }); +}; + +// --- API Endpoints --- +app.get('/api/stacks', async (req, res) => { + try { + const stacksRes = await axiosInstance.get('/api/stacks'); + + // Filter nach Endpoint-ID + const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); + + // Deduplication nach Name: nur einmal pro Name + const uniqueStacksMap = {}; + filteredStacks.forEach(stack => { + if (!uniqueStacksMap[stack.Name]) { + uniqueStacksMap[stack.Name] = stack; + } + }); + const uniqueStacks = Object.values(uniqueStacksMap); + + const stacksWithStatus = await Promise.all( + uniqueStacks.map(async (stack) => { + try { + const statusRes = await axiosInstance.get( + `/api/stacks/${stack.Id}/images_status?refresh=true` + ); + const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; + return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; + } catch { + return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; + } + }) + ); + + stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + res.json(stacksWithStatus); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +app.put('/api/stacks/:id/redeploy', async (req, res) => { + const { id } = req.params; + try { + broadcastRedeployStatus(id, true); + + const stackRes = await axiosInstance.get(`/api/stacks/${id}`); + const stack = stackRes.data; + + // Prüfen, ob Stack zum konfigurierten Endpoint gehört + if (stack.EndpointId !== ENDPOINT_ID) { + throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); + } + + if (stack.Type === 1) { + await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); + } else if (stack.Type === 2) { + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); + const stackFileContent = fileRes.data?.StackFileContent; + if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); + + const services = fileRes.data?.Config?.services || {}; + for (const serviceName in services) { + const imageName = services[serviceName].image; + if (!imageName) continue; + try { + await axiosInstance.post( + `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` + ); + } catch {} + } + + await axiosInstance.put(`/api/stacks/${id}`, + { StackFileContent: stackFileContent, Prune: false, PullImage: true }, + { params: { endpointId: stack.EndpointId } } + ); + } + + broadcastRedeployStatus(id, false); + res.json({ success: true, message: 'Stack redeployed' }); + } catch (err) { + broadcastRedeployStatus(id, false); + res.status(500).json({ error: err.message }); + } +}); + +// Server starten +server.listen(PORT, '0.0.0.0', () => { + console.log(`Backend läuft auf Port ${PORT}`); +}); diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 8b7bf27..6c930a4 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,35 +6,30 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - // Map: { [stackId]: boolean } - const [redeploying, setRedeploying] = useState({}); + const [redeploying, setRedeploying] = useState({}); // { [stackId]: boolean } // ------------------------- // WebSocket: connect + events // ------------------------- useEffect(() => { - // wenn Frontend und Backend auf verschiedenen Hosts/Ports laufen: - // const socket = io("http://localhost:4001"); - const socket = io(); // gleiche Origin (Express static) -> passt normalerweise - socket.on("connect", () => console.log("Socket connected:", socket.id)); + const socket = io(); // gleiche Origin + socket.on("connect", () => console.log("🔌 Socket connected:", socket.id)); socket.on("redeployStatus", async ({ stackId, status }) => { - // sofort den lokalen map-state setzen (optimistisch) setRedeploying(prev => ({ ...prev, [stackId]: status })); - // Wenn der Redeploy zu false geht, holen wir die aktuellen Stacks (aktualisieren UI) if (!status) { try { const res = await axios.get("/api/stacks"); const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); - // Map aus API-Daten aufbauen (wichtig für F5 / Konsistenz) + // Map aus API-Daten aufbauen const map = {}; sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); setRedeploying(map); } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + console.error("Fehler beim Aktualisieren der Stacks:", err); } } }); @@ -46,7 +41,7 @@ export default function Stacks() { }, []); // ------------------------- - // Initiale Stacks laden (auch setzt redeploying map) + // Initiale Stacks laden // ------------------------- const fetchStacks = async () => { setLoading(true); @@ -55,7 +50,7 @@ export default function Stacks() { const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); - // Wichtig: redeploying-Map aus API-Daten setzen, damit F5 den echten Zustand zeigt + // Map aus API-Daten setzen const map = {}; sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); setRedeploying(map); @@ -75,15 +70,13 @@ export default function Stacks() { // Redeploy Trigger // ------------------------- const handleRedeploy = async (stackId) => { - // sofort UI-Feedback setRedeploying(prev => ({ ...prev, [stackId]: true })); try { await axios.put(`/api/stacks/${stackId}/redeploy`); - // kein sofortiges setRedeploying(false) – Backend sendet das finale Event + // Backend sendet Event → UI wird dann automatisch zurückgesetzt } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - // Fehlerfall: wieder auf false setzen damit UI nicht hängen bleibt setRedeploying(prev => ({ ...prev, [stackId]: false })); } }; @@ -102,9 +95,8 @@ export default function Stacks() { return (
"gedämpfter" Container (sichtbar deaktiviert) className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition - ${isRedeploying ? "bg-gray-700 opacity-60" : "bg-gray-800 hover:bg-gray-700"}`} + ${isRedeploying ? "bg-gray-700 opacity-60 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`} aria-disabled={isRedeploying} >
@@ -119,7 +111,6 @@ export default function Stacks() {
- {/* Button ist jetzt immer sichtbar */} - )} + {/* Button ist jetzt immer sichtbar */} + ); })} + {stacks.length === 0 &&

Keine Stacks gefunden.

} ); -- 2.52.0 From 55a9c3327f6dc03233462eea60c158c40be16b09 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 08:20:39 +0000 Subject: [PATCH 25/26] Update --- backend/index.js | 28 ++++------------ backend/index.js.bak | 28 ++++++++++++---- frontend/src/Stacks.jsx | 73 ++++++++++++++--------------------------- 3 files changed, 53 insertions(+), 76 deletions(-) diff --git a/backend/index.js b/backend/index.js index c9f6f97..1a26dfc 100644 --- a/backend/index.js +++ b/backend/index.js @@ -48,7 +48,7 @@ const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("🔌 Client verbunden:", socket.id); + console.log("Client verbunden:", socket.id); }); const broadcastRedeployStatus = (stackId, status) => { @@ -59,13 +59,12 @@ const broadcastRedeployStatus = (stackId, status) => { // --- API Endpoints --- app.get('/api/stacks', async (req, res) => { try { - console.log("📦 Stacks werden von Portainer abgefragt …"); const stacksRes = await axiosInstance.get('/api/stacks'); - + // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); - // Deduplication nach Name + // Deduplication nach Name: nur einmal pro Name const uniqueStacksMap = {}; filteredStacks.forEach(stack => { if (!uniqueStacksMap[stack.Name]) { @@ -91,17 +90,13 @@ app.get('/api/stacks', async (req, res) => { stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); res.json(stacksWithStatus); } catch (err) { - console.error("❌ Fehler beim Laden der Stacks:", err.message); res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; - const startTime = Date.now(); - try { - console.log(`🟢 Redeploy gestartet für Stack ${id} …`); broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); @@ -113,11 +108,8 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } if (stack.Type === 1) { - console.log(`📂 Git-Stack erkannt (${stack.Name}) → Git-Redeploy`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { - console.log(`🐳 Docker-Compose-Stack erkannt (${stack.Name}) → Images pullen & neu deployen`); - const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); @@ -127,29 +119,21 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { - console.log(`⬇️ Pulling image: ${imageName}`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch (err) { - console.warn(`⚠️ Konnte Image ${imageName} nicht pullen:`, err.message); - } + } catch {} } - await axiosInstance.put( - `/api/stacks/${id}`, + await axiosInstance.put(`/api/stacks/${id}`, { StackFileContent: stackFileContent, Prune: false, PullImage: true }, { params: { endpointId: stack.EndpointId } } ); } - const duration = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(`✅ Redeploy erfolgreich abgeschlossen für Stack ${id} (${stack.Name}) in ${duration}s`); - broadcastRedeployStatus(id, false); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { - console.error(`❌ Redeploy-Fehler für Stack ${id}:`, err.message); broadcastRedeployStatus(id, false); res.status(500).json({ error: err.message }); } @@ -157,5 +141,5 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { // Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`🚀 Backend läuft auf Port ${PORT}`); + console.log(`Backend läuft auf Port ${PORT}`); }); diff --git a/backend/index.js.bak b/backend/index.js.bak index 1a26dfc..c9f6f97 100644 --- a/backend/index.js.bak +++ b/backend/index.js.bak @@ -48,7 +48,7 @@ const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("Client verbunden:", socket.id); + console.log("🔌 Client verbunden:", socket.id); }); const broadcastRedeployStatus = (stackId, status) => { @@ -59,12 +59,13 @@ const broadcastRedeployStatus = (stackId, status) => { // --- API Endpoints --- app.get('/api/stacks', async (req, res) => { try { + console.log("📦 Stacks werden von Portainer abgefragt …"); const stacksRes = await axiosInstance.get('/api/stacks'); - + // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); - // Deduplication nach Name: nur einmal pro Name + // Deduplication nach Name const uniqueStacksMap = {}; filteredStacks.forEach(stack => { if (!uniqueStacksMap[stack.Name]) { @@ -90,13 +91,17 @@ app.get('/api/stacks', async (req, res) => { stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); res.json(stacksWithStatus); } catch (err) { + console.error("❌ Fehler beim Laden der Stacks:", err.message); res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; + const startTime = Date.now(); + try { + console.log(`🟢 Redeploy gestartet für Stack ${id} …`); broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); @@ -108,8 +113,11 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } if (stack.Type === 1) { + console.log(`📂 Git-Stack erkannt (${stack.Name}) → Git-Redeploy`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { + console.log(`🐳 Docker-Compose-Stack erkannt (${stack.Name}) → Images pullen & neu deployen`); + const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); @@ -119,21 +127,29 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { + console.log(`⬇️ Pulling image: ${imageName}`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch {} + } catch (err) { + console.warn(`⚠️ Konnte Image ${imageName} nicht pullen:`, err.message); + } } - await axiosInstance.put(`/api/stacks/${id}`, + await axiosInstance.put( + `/api/stacks/${id}`, { StackFileContent: stackFileContent, Prune: false, PullImage: true }, { params: { endpointId: stack.EndpointId } } ); } + const duration = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(`✅ Redeploy erfolgreich abgeschlossen für Stack ${id} (${stack.Name}) in ${duration}s`); + broadcastRedeployStatus(id, false); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { + console.error(`❌ Redeploy-Fehler für Stack ${id}:`, err.message); broadcastRedeployStatus(id, false); res.status(500).json({ error: err.message }); } @@ -141,5 +157,5 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { // Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`Backend läuft auf Port ${PORT}`); + console.log(`🚀 Backend läuft auf Port ${PORT}`); }); diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 6c930a4..9560ebd 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,54 +6,37 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); // { [stackId]: boolean } + const [redeploying, setRedeploying] = useState({}); - // ------------------------- - // WebSocket: connect + events - // ------------------------- + // WebSocket initialisieren useEffect(() => { - const socket = io(); // gleiche Origin - socket.on("connect", () => console.log("🔌 Socket connected:", socket.id)); + const socket = io("/"); + console.log("Socket connected"); socket.on("redeployStatus", async ({ stackId, status }) => { setRedeploying(prev => ({ ...prev, [stackId]: status })); + // Wenn Redeploy beendet, Stack-Status neu laden if (!status) { try { const res = await axios.get("/api/stacks"); const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); - - // Map aus API-Daten aufbauen - const map = {}; - sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); - setRedeploying(map); } catch (err) { - console.error("Fehler beim Aktualisieren der Stacks:", err); + console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); } } }); - return () => { - socket.off("redeployStatus"); - socket.disconnect(); - }; + return () => socket.disconnect(); }, []); - // ------------------------- - // Initiale Stacks laden - // ------------------------- + // Stacks initial laden const fetchStacks = async () => { - setLoading(true); try { const res = await axios.get("/api/stacks"); const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); setStacks(sortedStacks); - - // Map aus API-Daten setzen - const map = {}; - sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); - setRedeploying(map); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -66,66 +49,60 @@ export default function Stacks() { fetchStacks(); }, []); - // ------------------------- - // Redeploy Trigger - // ------------------------- + // Redeploy eines Stacks const handleRedeploy = async (stackId) => { setRedeploying(prev => ({ ...prev, [stackId]: true })); try { await axios.put(`/api/stacks/${stackId}/redeploy`); - // Backend sendet Event → UI wird dann automatisch zurückgesetzt } catch (err) { console.error("❌ Fehler beim Redeploy:", err); setRedeploying(prev => ({ ...prev, [stackId]: false })); } }; - // ------------------------- - // Render - // ------------------------- if (loading) return

Lade Stacks...

; if (error) return

{error}

; return (
{stacks.map(stack => { - const isRedeploying = Boolean(redeploying[stack.Id]); + const isRedeploying = redeploying[stack.Id] || false; + const isUpToDate = stack.updateStatus === "✅"; return (
+ {/* Status Indicator */}
+ > +

{stack.Name}

ID: {stack.Id}

- + {!isUpToDate && ( + + )}
); })} - {stacks.length === 0 &&

Keine Stacks gefunden.

}
); -- 2.52.0 From d49605001e55bbdf7699dcaf9a8fa03ea9df19fc Mon Sep 17 00:00:00 2001 From: root Date: Thu, 25 Sep 2025 09:18:19 +0000 Subject: [PATCH 26/26] Update --- backend/index.js | 48 ++++++++--------- backend/index.js.bak | 71 ++++++++++++------------- frontend/src/Stacks.jsx | 63 ++++++++++++----------- frontend/src/Stacks.jsx.bak | 100 +++++++++++++----------------------- frontend/vite.config.js | 24 +++++---- 5 files changed, 139 insertions(+), 167 deletions(-) diff --git a/backend/index.js b/backend/index.js index 1a26dfc..398d5a3 100644 --- a/backend/index.js +++ b/backend/index.js @@ -15,61 +15,49 @@ const __dirname = path.dirname(__filename); const app = express(); app.use(express.json()); -// Statische Frontend-Dateien ausliefern app.use(express.static(path.join(__dirname, 'public'))); -// SPA-Fallback für React-Router app.get('*', (req, res, next) => { if (req.path.startsWith('/api')) return next(); res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -// Backend-Port fix const PORT = 4001; - -// Endpoint-ID aus der env const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); -// HTTPS Agent für Self-Signed-Zertifikate const agent = new https.Agent({ rejectUnauthorized: false }); - -// Axios-Instance für Portainer const axiosInstance = axios.create({ httpsAgent: agent, headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, baseURL: process.env.PORTAINER_URL, }); -// In-Memory Redeploy-Status const redeployingStacks = {}; -// HTTP Server + Socket.IO const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("Client verbunden:", socket.id); + console.log(`🔌 [Socket] Client verbunden: ${socket.id}`); }); const broadcastRedeployStatus = (stackId, status) => { redeployingStacks[stackId] = status; io.emit("redeployStatus", { stackId, status }); + console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); }; // --- API Endpoints --- + app.get('/api/stacks', async (req, res) => { + console.log("ℹ️ [API] GET /api/stacks: Abruf gestartet"); try { const stacksRes = await axiosInstance.get('/api/stacks'); - - // Filter nach Endpoint-ID const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); - // Deduplication nach Name: nur einmal pro Name const uniqueStacksMap = {}; filteredStacks.forEach(stack => { - if (!uniqueStacksMap[stack.Name]) { - uniqueStacksMap[stack.Name] = stack; - } + if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; }); const uniqueStacks = Object.values(uniqueStacksMap); @@ -80,36 +68,46 @@ app.get('/api/stacks', async (req, res) => { `/api/stacks/${stack.Id}/images_status?refresh=true` ); const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; - return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; - } catch { + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false + }; + } catch (err) { + console.error(`❌ Fehler beim Abrufen des Status für Stack ${stack.Id}:`, err.message); return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + console.log(`✅ GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); res.json(stacksWithStatus); } catch (err) { + console.error(`❌ Fehler beim Abrufen der Stacks:`, err.message); res.status(500).json({ error: err.message }); } }); app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; + console.log(`🔄 PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); + try { broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); const stack = stackRes.data; - // Prüfen, ob Stack zum konfigurierten Endpoint gehört if (stack.EndpointId !== ENDPOINT_ID) { throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); } if (stack.Type === 1) { + console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${id}) wird redeployed`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { + console.log(`🔄 [Redeploy] Compose Stack "${stack.Name}" (${id}) wird redeployed`); const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); @@ -119,10 +117,13 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { + console.log(`🖼️ Pulling image "${imageName}" für Service "${serviceName}"`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); - } catch {} + } catch (err) { + console.error(`❌ Fehler beim Pulling von Image "${imageName}":`, err.message); + } } await axiosInstance.put(`/api/stacks/${id}`, @@ -132,14 +133,15 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { } broadcastRedeployStatus(id, false); + console.log(`✅ PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { broadcastRedeployStatus(id, false); + console.error(`❌ Fehler beim Redeploy von Stack ${id}:`, err.message); res.status(500).json({ error: err.message }); } }); -// Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`Backend läuft auf Port ${PORT}`); + console.log(`🚀 Backend läuft auf Port ${PORT}`); }); diff --git a/backend/index.js.bak b/backend/index.js.bak index c9f6f97..ee67420 100644 --- a/backend/index.js.bak +++ b/backend/index.js.bak @@ -15,32 +15,28 @@ const __dirname = path.dirname(__filename); const app = express(); app.use(express.json()); -// Statische Frontend-Dateien ausliefern +// Statische Dateien app.use(express.static(path.join(__dirname, 'public'))); -// SPA-Fallback für React-Router +// SPA-Fallback app.get('*', (req, res, next) => { if (req.path.startsWith('/api')) return next(); res.sendFile(path.join(__dirname, 'public', 'index.html')); }); -// Backend-Port fix const PORT = 4001; - -// Endpoint-ID aus der env const ENDPOINT_ID = Number(process.env.PORTAINER_ENDPOINT_ID); -// HTTPS Agent für Self-Signed-Zertifikate +// HTTPS Agent const agent = new https.Agent({ rejectUnauthorized: false }); -// Axios-Instance für Portainer +// Axios für Portainer const axiosInstance = axios.create({ httpsAgent: agent, headers: { "X-API-Key": process.env.PORTAINER_API_KEY }, baseURL: process.env.PORTAINER_URL, }); -// In-Memory Redeploy-Status const redeployingStacks = {}; // HTTP Server + Socket.IO @@ -48,29 +44,27 @@ const server = http.createServer(app); const io = new Server(server, { cors: { origin: "*" } }); io.on("connection", (socket) => { - console.log("🔌 Client verbunden:", socket.id); + console.log(`🔌 [Socket] Client verbunden: ${socket.id}`); }); const broadcastRedeployStatus = (stackId, status) => { redeployingStacks[stackId] = status; io.emit("redeployStatus", { stackId, status }); + console.log(`🔄 [RedeployStatus] Stack ${stackId} ist jetzt ${status ? "im Redeploy" : "fertig"}`); }; // --- API Endpoints --- -app.get('/api/stacks', async (req, res) => { - try { - console.log("📦 Stacks werden von Portainer abgefragt …"); - const stacksRes = await axiosInstance.get('/api/stacks'); - // Filter nach Endpoint-ID +// Stacks abrufen +app.get('/api/stacks', async (req, res) => { + console.log("ℹ️ [API] GET /api/stacks: Abruf gestartet"); + try { + const stacksRes = await axiosInstance.get('/api/stacks'); const filteredStacks = stacksRes.data.filter(stack => stack.EndpointId === ENDPOINT_ID); - // Deduplication nach Name const uniqueStacksMap = {}; filteredStacks.forEach(stack => { - if (!uniqueStacksMap[stack.Name]) { - uniqueStacksMap[stack.Name] = stack; - } + if (!uniqueStacksMap[stack.Name]) uniqueStacksMap[stack.Name] = stack; }); const uniqueStacks = Object.values(uniqueStacksMap); @@ -81,43 +75,45 @@ app.get('/api/stacks', async (req, res) => { `/api/stacks/${stack.Id}/images_status?refresh=true` ); const statusEmoji = statusRes.data.Status === 'outdated' ? '⚠️' : '✅'; - return { ...stack, updateStatus: statusEmoji, redeploying: redeployingStacks[stack.Id] || false }; - } catch { + return { + ...stack, + updateStatus: statusEmoji, + redeploying: redeployingStacks[stack.Id] || false + }; + } catch (err) { + console.error(`❌ [API] Fehler beim Abrufen Status Stack ${stack.Id}:`, err.message); return { ...stack, updateStatus: '❌', redeploying: redeployingStacks[stack.Id] || false }; } }) ); stacksWithStatus.sort((a, b) => a.Name.localeCompare(b.Name)); + console.log(`✅ [API] GET /api/stacks: Abruf erfolgreich, ${stacksWithStatus.length} Stacks geladen`); res.json(stacksWithStatus); } catch (err) { - console.error("❌ Fehler beim Laden der Stacks:", err.message); + console.error(`❌ [API] Fehler beim Abrufen der Stacks:`, err.message); res.status(500).json({ error: err.message }); } }); +// Stack redeployen app.put('/api/stacks/:id/redeploy', async (req, res) => { const { id } = req.params; - const startTime = Date.now(); + console.log(`🔄 [API] PUT /api/stacks/${id}/redeploy: Redeploy gestartet`); try { - console.log(`🟢 Redeploy gestartet für Stack ${id} …`); broadcastRedeployStatus(id, true); const stackRes = await axiosInstance.get(`/api/stacks/${id}`); const stack = stackRes.data; - // Prüfen, ob Stack zum konfigurierten Endpoint gehört - if (stack.EndpointId !== ENDPOINT_ID) { - throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); - } + if (stack.EndpointId !== ENDPOINT_ID) throw new Error(`Stack gehört nicht zum Endpoint ${ENDPOINT_ID}`); if (stack.Type === 1) { - console.log(`📂 Git-Stack erkannt (${stack.Name}) → Git-Redeploy`); + console.log(`🔄 [Redeploy] Git Stack "${stack.Name}" (${id}) wird redeployed`); await axiosInstance.put(`/api/stacks/${id}/git/redeploy?endpointId=${stack.EndpointId}`); } else if (stack.Type === 2) { - console.log(`🐳 Docker-Compose-Stack erkannt (${stack.Name}) → Images pullen & neu deployen`); - + console.log(`🔄 [Redeploy] Compose Stack "${stack.Name}" (${id}) wird redeployed`); const fileRes = await axiosInstance.get(`/api/stacks/${id}/file`); const stackFileContent = fileRes.data?.StackFileContent; if (!stackFileContent) throw new Error("Stack file konnte nicht geladen werden"); @@ -127,35 +123,32 @@ app.put('/api/stacks/:id/redeploy', async (req, res) => { const imageName = services[serviceName].image; if (!imageName) continue; try { - console.log(`⬇️ Pulling image: ${imageName}`); + console.log(`🖼️ [Redeploy] Pulling image "${imageName}" für Service "${serviceName}"`); await axiosInstance.post( `/api/endpoints/${stack.EndpointId}/docker/images/create?fromImage=${encodeURIComponent(imageName)}` ); } catch (err) { - console.warn(`⚠️ Konnte Image ${imageName} nicht pullen:`, err.message); + console.error(`❌ [Redeploy] Fehler beim Pulling von Image "${imageName}":`, err.message); } } - await axiosInstance.put( - `/api/stacks/${id}`, + await axiosInstance.put(`/api/stacks/${id}`, { StackFileContent: stackFileContent, Prune: false, PullImage: true }, { params: { endpointId: stack.EndpointId } } ); } - const duration = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(`✅ Redeploy erfolgreich abgeschlossen für Stack ${id} (${stack.Name}) in ${duration}s`); - broadcastRedeployStatus(id, false); + console.log(`✅ [API] PUT /api/stacks/${id}/redeploy: Redeploy erfolgreich abgeschlossen`); res.json({ success: true, message: 'Stack redeployed' }); } catch (err) { - console.error(`❌ Redeploy-Fehler für Stack ${id}:`, err.message); broadcastRedeployStatus(id, false); + console.error(`❌ [API] Fehler beim Redeploy von Stack ${id}:`, err.message); res.status(500).json({ error: err.message }); } }); // Server starten server.listen(PORT, '0.0.0.0', () => { - console.log(`🚀 Backend läuft auf Port ${PORT}`); + console.log(`🚀 [Server] Backend läuft auf Port ${PORT}`); }); diff --git a/frontend/src/Stacks.jsx b/frontend/src/Stacks.jsx index 9560ebd..cc6c353 100644 --- a/frontend/src/Stacks.jsx +++ b/frontend/src/Stacks.jsx @@ -6,37 +6,40 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - const [redeploying, setRedeploying] = useState({}); - // WebSocket initialisieren useEffect(() => { - const socket = io("/"); - console.log("Socket connected"); + const socket = io("/", { transports: ["websocket"] }); + console.log("🔌 Socket connected"); socket.on("redeployStatus", async ({ stackId, status }) => { - setRedeploying(prev => ({ ...prev, [stackId]: status })); + console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); - // Wenn Redeploy beendet, Stack-Status neu laden if (!status) { + // Status nach Redeploy neu vom Server holen try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.sort((a, b) => a.Name.localeCompare(b.Name))); } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); + console.error("Fehler beim Aktualisieren nach Redeploy:", err); } + } else { + // UI direkt auf redeploying setzen + setStacks(prev => + prev.map(stack => + stack.Id === stackId ? { ...stack, redeploying: true } : stack + ) + ); } }); return () => socket.disconnect(); }, []); - // Stacks initial laden const fetchStacks = async () => { + setLoading(true); try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); + setStacks(res.data.map(stack => ({ ...stack, redeploying: stack.redeploying || false }))); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -49,15 +52,19 @@ export default function Stacks() { fetchStacks(); }, []); - // Redeploy eines Stacks const handleRedeploy = async (stackId) => { - setRedeploying(prev => ({ ...prev, [stackId]: true })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack) + ); try { await axios.put(`/api/stacks/${stackId}/redeploy`); + // Socket.IO Event aktualisiert Status automatisch } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - setRedeploying(prev => ({ ...prev, [stackId]: false })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack) + ); } }; @@ -67,8 +74,7 @@ export default function Stacks() { return (
{stacks.map(stack => { - const isRedeploying = redeploying[stack.Id] || false; - const isUpToDate = stack.updateStatus === "✅"; + const isRedeploying = stack.redeploying; return (
- {/* Status Indicator */}
-
+ />

{stack.Name}

ID: {stack.Id}

- {!isUpToDate && ( - - )} +
); })} diff --git a/frontend/src/Stacks.jsx.bak b/frontend/src/Stacks.jsx.bak index 8b7bf27..e890c62 100644 --- a/frontend/src/Stacks.jsx.bak +++ b/frontend/src/Stacks.jsx.bak @@ -6,59 +6,36 @@ export default function Stacks() { const [stacks, setStacks] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - // Map: { [stackId]: boolean } - const [redeploying, setRedeploying] = useState({}); - // ------------------------- - // WebSocket: connect + events - // ------------------------- + // Socket.IO initialisieren (Proxy leitet /socket.io an Backend) useEffect(() => { - // wenn Frontend und Backend auf verschiedenen Hosts/Ports laufen: - // const socket = io("http://localhost:4001"); - const socket = io(); // gleiche Origin (Express static) -> passt normalerweise - socket.on("connect", () => console.log("Socket connected:", socket.id)); + const socket = io("/", { transports: ["websocket"] }); + console.log("🔌 Socket connected"); - socket.on("redeployStatus", async ({ stackId, status }) => { - // sofort den lokalen map-state setzen (optimistisch) - setRedeploying(prev => ({ ...prev, [stackId]: status })); - - // Wenn der Redeploy zu false geht, holen wir die aktuellen Stacks (aktualisieren UI) - if (!status) { - try { - const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - - // Map aus API-Daten aufbauen (wichtig für F5 / Konsistenz) - const map = {}; - sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); - setRedeploying(map); - } catch (err) { - console.error("Fehler beim Aktualisieren des Status nach Redeploy:", err); - } - } + socket.on("redeployStatus", ({ stackId, status }) => { + console.log(`🔄 Stack ${stackId} Redeploy Status: ${status ? "running" : "finished"}`); + setStacks(prevStacks => + prevStacks.map(stack => + stack.Id === stackId + ? { ...stack, redeploying: status, updateStatus: status ? stack.updateStatus : "✅" } + : stack + ) + ); }); - return () => { - socket.off("redeployStatus"); - socket.disconnect(); - }; + return () => socket.disconnect(); }, []); - // ------------------------- - // Initiale Stacks laden (auch setzt redeploying map) - // ------------------------- + // Stacks initial laden const fetchStacks = async () => { setLoading(true); try { const res = await axios.get("/api/stacks"); - const sortedStacks = res.data.sort((a, b) => a.Name.localeCompare(b.Name)); - setStacks(sortedStacks); - - // Wichtig: redeploying-Map aus API-Daten setzen, damit F5 den echten Zustand zeigt - const map = {}; - sortedStacks.forEach(s => { map[s.Id] = !!s.redeploying; }); - setRedeploying(map); + setStacks( + res.data + .sort((a, b) => a.Name.localeCompare(b.Name)) + .map(stack => ({ ...stack, redeploying: stack.redeploying || false })) + ); } catch (err) { console.error("❌ Fehler beim Abrufen der Stacks:", err); setError("Fehler beim Laden der Stacks"); @@ -71,43 +48,40 @@ export default function Stacks() { fetchStacks(); }, []); - // ------------------------- - // Redeploy Trigger - // ------------------------- + // Redeploy starten const handleRedeploy = async (stackId) => { - // sofort UI-Feedback - setRedeploying(prev => ({ ...prev, [stackId]: true })); + // Sofort UI auf redeploying setzen + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: true } : stack) + ); try { await axios.put(`/api/stacks/${stackId}/redeploy`); - // kein sofortiges setRedeploying(false) – Backend sendet das finale Event + // Backend sendet Socket-Event → UI aktualisiert automatisch } catch (err) { console.error("❌ Fehler beim Redeploy:", err); - // Fehlerfall: wieder auf false setzen damit UI nicht hängen bleibt - setRedeploying(prev => ({ ...prev, [stackId]: false })); + setStacks(prev => + prev.map(stack => stack.Id === stackId ? { ...stack, redeploying: false } : stack) + ); } }; - // ------------------------- - // Render - // ------------------------- if (loading) return

Lade Stacks...

; if (error) return

{error}

; return (
{stacks.map(stack => { - const isRedeploying = Boolean(redeploying[stack.Id]); + const isRedeploying = stack.redeploying; return (
"gedämpfter" Container (sichtbar deaktiviert) className={`flex justify-between items-center p-5 rounded-xl shadow-lg transition - ${isRedeploying ? "bg-gray-700 opacity-60" : "bg-gray-800 hover:bg-gray-700"}`} - aria-disabled={isRedeploying} + ${isRedeploying ? "bg-gray-700 cursor-not-allowed" : "bg-gray-800 hover:bg-gray-700"}`} >
+ {/* Status Indicator */}
- {/* Button ist jetzt immer sichtbar */} + {/* Redeploy Button */}
); })} - {stacks.length === 0 &&

Keine Stacks gefunden.

}
); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index aa3ae76..d33ca44 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,17 +1,21 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], server: { - host: true, - port: 5173, + host: true, // Hört auf allen Netzwerk-Interfaces, kein localhost notwendig + port: 5173, // optional, Standardport für Vite proxy: { - "/api": { - target: "http://127.0.0.1:4001", // dein Backend - changeOrigin: true, + '/api': { + target: 'http://127.0.0.1:4001', // Backend lokal auf Port 4001 + changeOrigin: true }, - }, - allowedHosts: "all", - }, + '/socket.io': { + target: 'http://127.0.0.1:4001', // WebSocket-Verbindungen über Proxy weiterleiten + ws: true, + changeOrigin: true + } + } + } }); -- 2.52.0