{"version":3,"file":"main.c23d1469027f5105171c.css","mappings":";AA+EA;IACI,YAAY;AAChB;;;ACoEA;EACE,2BAA2B;AAC7B;;;ACsTA;IACI,aAAa;AACjB;AAEA;IACI,aAAa;AACjB;;;;AC6GA;IACI,+CAAsC;AAC1C;AAEA;IACI,wBAAwB;AAC5B;AACI;AACA;QACI,mCAAmC;AACvC;AACA;QACI,qCAAqC;AACzC;AACA;QACI,wCAAwC;AAC5C;AACJ;;;AC0LA;IACI,qBAAqB;IACrB,2BAA2B;IAC3B,eAAU;SAAV,UAAU;AACd;AACA;IACI,gBAAgB;IAChB,uBAAuB;IACvB,mBAAmB;AACvB;;;ACzjBA;EACE;;;;;;GAMC;AACH;;;ACrMA;IACI,aAAa;AACjB;;AChCA,iEAAc,CAAd,8FAAc;;AAAd;;;CAAc;;AAAd;;CAAc;;AAAd;;;CCcC,sBAAsB;ADdT;;AAAd;;CAAc;;AAAd;CCsBC,gBAAgB;CAChB,cAAW;IAAX,WAAW;ADvBE;;AAAd;;;CAAc;;AAAd;CCgCC,iBAAiB,EAAE,MAAM;CACzB,8BAA8B,EAAE,MAAM;ADjCzB;;AAAd;;;CAAc;;AAAd;;CAAc;;AAAd;CC8CC,SAAS;AD9CI;;AAAd;;CAAc;;AAAd;CCsDC;;;;;;;;;kBASiB;AD/DJ;;AAAd;;;CAAc;;AAAd;;;CAAc;;AAAd;CC6EC,SAAS,EAAE,MAAM;CACjB,cAAc,EAAE,MAAM;AD9ET;;AAAd;;;CAAc;;AAAd;;CAAc;;AAAd;CC2FC,yCAAiC;SAAjC,iCAAiC;AD3FpB;;AAAd;;CAAc;;AAAd;;CCoGC,mBAAmB;ADpGN;;AAAd;;;CAAc;;AAAd;;;;CCgHC;;;;;;WAMU,EAAE,MAAM;CAClB,cAAc,EAAE,MAAM;ADvHT;;AAAd;;CAAc;;AAAd;CC+HC,cAAc;AD/HD;;AAAd;;CAAc;;AAAd;;CCwIC,cAAc;CACd,cAAc;CACd,kBAAkB;CAClB,wBAAwB;AD3IX;;AAAd;CC+IC,eAAe;AD/IF;;AAAd;CCmJC,WAAW;ADnJE;;AAAd;;;CAAc;;AAAd;;;CAAc;;AAAd;CCiKC,cAAc,EAAE,MAAM;CACtB,qBAAqB,EAAE,MAAM;ADlKhB;;AAAd;;;CAAc;;AAAd;;;CAAc;;AAAd;;;;;CCoLC,oBAAoB,EAAE,MAAM;CAC5B,eAAe,EAAE,MAAM;CACvB,iBAAiB,EAAE,MAAM;CACzB,SAAS,EAAE,MAAM;ADvLJ;;AAAd;;;CAAc;;AAAd;SCgMS,MAAM;CACd,oBAAoB;ADjMP;;AAAd;;CAAc;;AAAd;;;;CC4MC,0BAA0B;AD5Mb;;AAAd;;CAAc;;AAAd;CCoNC,kBAAkB;CAClB,UAAU;ADrNG;;AAAd;;CAAc;;AAAd;CC6NC,8BAA8B;AD7NjB;;AAAd;;;CAAc;;AAAd;CCsOC,gBAAgB;ADtOH;;AAAd;;CAAc;;AAAd;CC8OC,UAAU;AD9OG;;AAAd;;CAAc;;AAAd;CCsPC,wBAAwB;ADtPX;;AAAd;;CAAc;;AAAd;;CC+PC,YAAY;AD/PC;;AAAd;;;CAAc;;AAAd;CCwQC,6BAA6B,EAAE,MAAM;CACrC,oBAAoB,EAAE,MAAM;ADzQf;;AAAd;;CAAc;;AAAd;CCiRC,wBAAwB;ADjRX;;AAAd;;;CAAc;;AAAd;CC0RC,0BAA0B,EAAE,MAAM;CAClC,aAAa,EAAE,MAAM;AD3RR;;AAAd;;;CAAc;;AAAd;;CAAc;;AAAd;CCwSC,kBAAkB;ADxSL,CAAd;;;;EAAc;;AAAd;;EAAc;;AAAd;;;;;;;;;;;;;EEuBE,SAAS;AFvBG;;AAAd;EE2BE,6BAA6B;EAC7B,sBAAsB;AF5BV;;AAAd;EEgCE,SAAS;EACT,UAAU;AFjCE;;AAAd;;EEsCE,gBAAgB;EAChB,SAAS;EACT,UAAU;AFxCE;;AAAd;;EAAc;;AAAd;;;;;EAAc;;AAAd;EEuDE,4NAAsP,EAAE,MAAM;EAC9P,gBAAgB,EAAE,MAAM;AFxDZ;;;AAAd;;;EAAc;;AAAd;EEkEE,oBAAoB;EACpB,oBAAoB;AFnER;;AAAd;;;;;;;;;;;;;;;;;;;;;;;;EAAc;;AAAd;;;EEmGE,sBAAsB,EAAE,MAAM;EAC9B,eAAe,EAAE,MAAM;EACvB,mBAAmB,EAAE,MAAM;EAC3B,0BAA0B,EAAE,MAAM;AFtGtB;;AAAd;;EAAc;;AAAd;EE8GE,qBAAqB;AF9GT;;AAAd;;;;;;;;EAAc;;AAAd;EE4HE,mBAAmB;AF5HP;;AAAd;EEgIE,gBAAgB;AFhIJ;;AAAd;EEqIE,UAAU;EACV,cAAwC;AFtI5B;;AAAd;;EEqIE,UAAU;EACV,cAAwC;AFtI5B;;AAAd;;EE2IE,eAAe;AF3IH;;AAAd;;;;;;EAAc;;AAAd;CEuJC,aAAa;AFvJA;;AAAd;EE2JE,yBAAyB;AF3Jb;;AAAd;;;;;;EEoKE,kBAAkB;EAClB,oBAAoB;AFrKR;;AAAd;;;EAAc;;AAAd;EE8KE,cAAc;EACd,wBAAwB;AF/KZ;;AAAd;;;;;;EAAc;;AAAd;;;;;EE+LE,UAAU;EACV,oBAAoB;EACpB,cAAc;AFjMF;;AAAd;;;;;EAAc;;AAAd;;;;EE+ME,+GAAyI;AF/M7H;;AAAd;;;;;;;;;;;;;;;EAAc;;AAAd;;;;;;;;EE2OE,cAAc,EAAE,MAAM;EACtB,sBAAsB,EAAE,MAAM;AF5OlB;;AAAd;;;;;EAAc;;AAAd;;EEwPE,eAAe;EACf,YAAY;AFzPA;;AAAd;;EAAc;;AAAd;EEiQE,aAAa;AFjQD;;AGAd;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IHAA;;;;;;;;;;;OAAc;;IGAd;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;IAAA;SAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;;IAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;;IHAA,gFAAc;IGAd;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;IAAA;CAAA;CAAA;CAAA;AHCA;CGDA;AHCoB;AAApB;;CGDA;EAAA;EAAA;AHCoB;AAApB;;CGDA;EAAA;EAAA;AHCoB;AAApB;;CGDA;EAAA;EAAA;AHCoB;AAApB;;CGDA;EAAA;EAAA;AHCoB;AAApB;;CGDA;EAAA;EAAA;AHCoB;AGDpB;CAAA;CAAA;AAAA;;CAAA;EAAA;EAAA;CAAA;AAAA;CAAA;CAAA;CAAA;AAAA;;CAAA;EAAA;EAAA;CAAA;AHEA;CGFA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;;CGFA;EAAA;EAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;IAAA;SAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;MAAA;AHEmB;AAAnB;CGFA;MAAA;AHEmB;AAAnB;CGFA;MAAA;AHEmB;AAAnB;CGFA;MAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;CAAA;CAAA;CAAA;CAAA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAAnB;CGFA;AHEmB;AAFnB;CGAA;AH4RC;AA5RD;CGAA;CAAA;AH4RC;AA5RD;CGAA;CAAA;AH4RC;AA5RD;CGAA;CAAA;AH4RC;AA5RD;CGAA;CAAA;AH4RC;AA5RD;CGAA;AH4RC;AA5RD;CGAA;CAAA;AH4RC;AA5RD;;CGAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;AH4RC;AA5RD;;CGAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;AH4RC;AA5RD;;CGAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;EAAA;AH4RC;AA5RD;;CGAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;AH4RC;AA5RD;;CGAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;;CAAA;EAAA;EAAA;AH4RC,C","sources":["webpack://@flowfuse/flowfuse/./frontend/src/components/dialogs/AssetDetailDialog.vue","webpack://@flowfuse/flowfuse/./frontend/src/components/dialogs/AssetCompareDialog.vue","webpack://@flowfuse/flowfuse/./frontend/src/pages/device/VersionHistory/Snapshots/index.vue","webpack://@flowfuse/flowfuse/./frontend/src/pages/device/index.vue","webpack://@flowfuse/flowfuse/./frontend/src/components/DevicesBrowser.vue","webpack://@flowfuse/flowfuse/./frontend/src/pages/instance/components/InstanceLogs.vue","webpack://@flowfuse/flowfuse/./frontend/src/components/flow-viewer/FlowViewer.vue","webpack://@flowfuse/flowfuse/./frontend/src/index.css","webpack://@flowfuse/flowfuse/./frontend/src/%3Cinput%20css%20aXXsRI%3E","webpack://@flowfuse/flowfuse/./frontend/src/%3Cinput%20css%20pf0nCj%3E","webpack://@flowfuse/flowfuse/<no source>"],"sourcesContent":["<template>\n    <ff-dialog\n        ref=\"dialog\" :header=\"header\" :sub-header=\"`Node-RED Version: ${nrVersion}`\" confirm-label=\"Close\" :closeOnConfirm=\"true\"\n        data-el=\"flow-view-dialog\" boxClass=\"!min-w-[80%] !min-h-[80%] !w-[80%] !h-[80%]\"\n        contentClass=\"overflow-hidden flex-grow\" @confirm=\"confirm()\"\n    >\n        <template #default>\n            <div ref=\"viewer\" data-el=\"ff-flow-previewer\" class=\"ff-flow-viewer\" @click.stop.prevent>\n                Loading...\n            </div>\n        </template>\n        <template #actions>\n            <div class=\"flex justify-end\">\n                <ff-button data-action=\"dialog-confirm\" @click=\"confirm()\">Close</ff-button>\n            </div>\n        </template>\n    </ff-dialog>\n</template>\n<script>\n\nimport FlowRenderer from '@flowfuse/flow-renderer'\n\nexport default {\n    name: 'FlowViewerDialog',\n    props: {\n        title: {\n            type: String,\n            default: ''\n        }\n    },\n    setup () {\n        return {\n            show (payload) { // accepts blueprints, snapshots and libraries\n                this.mode = 'view'\n                this.$refs.dialog.show()\n                this.payload = payload\n                setTimeout(() => {\n                    this.renderFlows()\n                }, 20)\n            }\n        }\n    },\n    data () {\n        return {\n            payload: []\n        }\n    },\n    computed: {\n        flow () {\n            return this.payload?.flows?.flows || []\n        },\n        nrVersion () {\n            const mods = this.payload?.settings?.modules\n            if (mods) {\n                return mods['node-red'] || 'Unavailable'\n            }\n            return ''\n        },\n        header () {\n            return this.payload?.name || this.title || 'Flow'\n        }\n    },\n    mounted () {\n    },\n    methods: {\n        confirm () {\n            this.$refs.dialog.close()\n        },\n        renderFlows () {\n            const flowRenderer = new FlowRenderer()\n            flowRenderer.renderFlows(this.flow, {\n                container: this.$refs.viewer\n            })\n        }\n    }\n}\n</script>\n\n<style scoped>\n.ff-flow-viewer {\n    height: 100%;\n}\n</style>\n","<template>\n    <ff-dialog\n        ref=\"dialog\" :header=\"header\" confirm-label=\"Close\" :closeOnConfirm=\"true\" data-el=\"flow-view-dialog\"\n        boxClass=\"!min-w-[80%] !min-h-[80%] !w-[80%] !h-[80%]\" contentClass=\"overflow-hidden flex-grow\"\n        @confirm=\"confirm()\"\n    >\n        <template #default>\n            <div class=\"flex gap-2\" data-el=\"snapshot-compare-toolbar\">\n                <ff-listbox\n                    v-model=\"compareSnapshot\"\n                    :options=\"compareSnapshotList\"\n                    data-el=\"snapshots-list\"\n                    label-key=\"label\"\n                    option-title-key=\"description\"\n                    class=\"flex-grow\"\n                />\n                <ff-button\n                    v-if=\"true\"\n                    :disabled=\"!compareSnapshot\"\n                    data-action=\"compare-snapshots\"\n                    kind=\"secondary\"\n                    style=\"height: 30px; width: 106px\"\n                    class=\"w-32\"\n                    @click=\"renderComparison\"\n                >\n                    Compare\n                </ff-button>\n            </div>\n            <div v-if=\"changes.length\" class=\"flex justify-between items-center gap-2 mt-2 ml-2\">\n                <div class=\"whitespace-nowrap\">Change {{ changeIndex + 1 }} of {{ changes.length }}:</div>\n                <div class=\"text-sm text-gray-500 flex-grow truncate overflow-ellipsis\">\n                    {{ changes[changeIndex].toString() }}\n                </div>\n                <ff-button kind=\"secondary\" size=\"small\" class=\"w-14\" @click=\"gotoPreviousDifference\">Prev</ff-button>\n                <ff-button kind=\"secondary\" size=\"small\" class=\"w-14\" @click=\"gotoNextDifference\">Next</ff-button>\n            </div>\n            <div v-else class=\"mt-2\">\n                <div class=\"text-sm text-gray-500 flex-grow truncate overflow-ellipsis ml-2\">No differences found</div>\n            </div>\n            <div ref=\"compareViewer\" data-el=\"ff-flow-compare-view\" class=\"ff-flow-compare-viewer pt-4\" @click.stop.prevent>\n              &nbsp;\n            </div>\n        </template>\n        <template #actions>\n            <div class=\"flex justify-end\">\n                <ff-button data-action=\"dialog-confirm\" @click=\"confirm()\">Close</ff-button>\n            </div>\n        </template>\n    </ff-dialog>\n</template>\n<script>\n\nimport FlowRenderer from '@flowfuse/flow-renderer'\n\nimport SnapshotsApi from '../../api/snapshots.js'\n\nimport Alerts from '../../services/alerts.js'\n\nexport default {\n    name: 'AssetCompareDialog',\n    props: {\n        title: {\n            type: String,\n            default: ''\n        }\n    },\n    setup () {\n        return {\n        /**\n         * Shows the compare flows dialog and presents the user with a list of snapshots to compare against\n         * @param {{flows: { flows :[]}}} v1Snapshot - A snapshot object as the base for comparison\n         * @param {[{label: String, value: String}]} snapshotList - A list of snapshots to compare against where label is the snapshot name and value is the snapshot id\n         */\n            show (v1Snapshot, snapshotList) {\n                this.mode = 'compare'\n                this.payload = v1Snapshot\n                this.compareSnapshot = null\n                this.changes = []\n                this.changeIndex = 0\n                this.compareSnapshotList = snapshotList\n                this.$refs.dialog.show()\n            }\n        }\n    },\n    data () {\n        return {\n            payload: [],\n            snapshotList: [],\n            compareSnapshot: null,\n            compareSnapshotList: [],\n            mode: 'view', // view, compare\n            changes: [],\n            changeIndex: 0\n        }\n    },\n    computed: {\n        flow () {\n            return this.payload?.flows?.flows || []\n        },\n        header () {\n            return this.payload?.name || this.title || 'Flow'\n        }\n    },\n    mounted () {\n    },\n    methods: {\n        confirm () {\n            this.cleanup()\n            this.$refs.dialog.close()\n        },\n        renderFlows () {\n            this.cleanup()\n            const flowRenderer = new FlowRenderer()\n            flowRenderer.renderFlows(this.flow, {\n                container: this.$refs.compareViewer\n            })\n        },\n        async renderComparison (snapshotId) {\n            this.cleanup()\n            const compareSnapshot = await SnapshotsApi.getFullSnapshot(this.compareSnapshot)\n            if (!compareSnapshot?.flows?.flows) {\n                Alerts.emit('Flows not found in the selected snapshot', 'warning')\n                return\n            }\n            const flowRenderer = new FlowRenderer()\n            const flows = [this.flow, compareSnapshot?.flows?.flows]\n            const result = flowRenderer.compare(flows, {\n                container: this.$refs.compareViewer\n            })\n            this.changes = result?.changes || []\n        },\n        gotoNextDifference () {\n            this.changeIndex = (this.changeIndex + 1) % this.changes.length\n            this.changes[this.changeIndex].highlight()\n        },\n        gotoPreviousDifference () {\n            this.changeIndex = (this.changeIndex - 1 + this.changes.length) % this.changes.length\n            this.changes[this.changeIndex].highlight()\n        },\n        cleanup () {\n            while (this.$refs.compareViewer?.firstChild) {\n                this.$refs.compareViewer.removeChild(this.$refs.compareViewer.firstChild)\n            }\n        }\n    }\n}\n</script>\n\n<style scoped>\n.ff-flow-compare-viewer {\n  height: calc(100% - 4.5rem);\n}\n</style>\n","<template>\n    <div id=\"device-snapshots\">\n        <div v-if=\"isOwnedByAnInstance || isUnassigned\" class=\"space-y-6\">\n            <EmptyState :feature-unavailable=\"!features.deviceEditor\">\n                <template #img>\n                    <img src=\"../../../../images/empty-states/instance-snapshots.png\">\n                </template>\n                <template #header>Snapshots are available when a Remote Instance is assigned to an Application</template>\n                <template #message>\n                    <p>\n                        Snapshots are point-in-time backups of your Node-RED Instances\n                        and capture the flows, credentials and runtime settings.\n                    </p>\n                    <p v-if=\"device.ownerType !== 'application'\" class=\"block\">\n                        A Remote Instance must first be <a class=\"ff-link\" href=\"https://flowfuse.com/docs/device-agent/register/#assign-the-device-to-an-application\" target=\"_blank\" rel=\"noreferrer\">assigned to an Application</a>, in order to create snapshots.\n                    </p>\n                    <p v-else-if=\"!developerMode\" class=\"block\">\n                        A Remote Instance must be in Developer Mode and online to create a Snapshot.\n                    </p>\n                </template>\n                <template v-if=\"hasPermission('device:snapshot:create')\" #actions>\n                    <ff-button\n                        v-if=\"hasPermission('snapshot:import')\"\n                        kind=\"secondary\" :disabled=\"busy || !features.deviceEditor || device.ownerType !== 'application'\"\n                        data-action=\"import-snapshot\"\n                        @click=\"$emit('show-import-snapshot-dialog')\"\n                    >\n                        <template #icon-left><UploadIcon /></template>Upload Snapshot\n                    </ff-button>\n                    <ff-button\n                        kind=\"primary\"\n                        :disabled=\"!developerMode || busy || !features.deviceEditor || device.ownerType !== 'application'\"\n                        data-action=\"create-snapshot\"\n                        @click=\"$emit('show-create-snapshot-dialog')\"\n                    >\n                        <template #icon-left><PlusSmIcon /></template>Create Snapshot\n                    </ff-button>\n                </template>\n            </EmptyState>\n        </div>\n        <div v-else class=\"space-y-6\">\n            <ff-loading v-if=\"loading\" message=\"Loading Snapshots...\" />\n            <template v-else-if=\"features.deviceEditor && snapshots.length > 0\">\n                <ff-data-table data-el=\"snapshots\" class=\"space-y-4\" :columns=\"columns\" :rows=\"snapshotsFiltered\" :show-search=\"true\" search-placeholder=\"Search Snapshots...\">\n                    <template #actions>\n                        <DropdownMenu data-el=\"snapshot-filter\" buttonClass=\"ff-btn ff-btn--secondary\" :options=\"snapshotFilterOptions\">\n                            <FilterIcon class=\"ff-btn--icon ff-btn--icon-left\" aria-hidden=\"true\" />\n                            {{ snapshotFilter?.name || 'All Snapshots' }}\n                            <span class=\"sr-only\">Filter Snapshots</span>\n                        </DropdownMenu>\n                    </template>\n                    <template #context-menu=\"{row}\">\n                        <ff-list-item :disabled=\"!canDeploy(row)\" label=\"Restore Snapshot\" @click=\"showDeploySnapshotDialog(row)\" />\n                        <ff-list-item :disabled=\"!hasPermission('snapshot:edit')\" label=\"Edit Snapshot\" @click=\"showEditSnapshotDialog(row)\" />\n                        <ff-list-item :disabled=\"!hasPermission('snapshot:full')\" label=\"View Snapshot\" @click=\"showViewSnapshotDialog(row)\" />\n                        <ff-list-item :disabled=\"!hasPermission('snapshot:full')\" label=\"Compare Snapshot...\" @click=\"showCompareSnapshotDialog(row)\" />\n                        <ff-list-item :disabled=\"!canDownload(row)\" label=\"Download Snapshot\" @click=\"showDownloadSnapshotDialog(row)\" />\n                        <ff-list-item :disabled=\"!hasPermission('device:snapshot:read')\" label=\"Download package.json\" @click=\"downloadSnapshotPackage(row)\" />\n                        <ff-list-item :disabled=\"!canDelete(row)\" label=\"Delete Snapshot\" kind=\"danger\" @click=\"showDeleteSnapshotDialog(row)\" />\n                    </template>\n                </ff-data-table>\n            </template>\n            <template v-else-if=\"!loading\">\n                <EmptyState :feature-unavailable=\"!features.deviceEditor\" :feature-unavailable-message=\"'This requires Developer Mode on Devices, which is a FlowFuse Enterprise Feature'\">\n                    <template #img>\n                        <img src=\"../../../../images/empty-states/instance-snapshots.png\">\n                    </template>\n                    <template #header>Create your First Snapshot</template>\n                    <template #message>\n                        <p>\n                            Snapshots are point-in-time backups of your Node-RED Instances\n                            and capture the flows, credentials and runtime settings.\n                        </p>\n                        <p v-if=\"device.ownerType !== 'application'\" class=\"block\">\n                            A Remote Instance must first be <a class=\"ff-link\" href=\"https://flowfuse.com/docs/device-agent/register/#assign-the-device-to-an-application\" target=\"_blank\" rel=\"noreferrer\">assigned to an Application</a>, in order to create snapshots.\n                        </p>\n                        <p v-else-if=\"!developerMode\" class=\"block\">\n                            A Remote Instance must be in Developer Mode and online to create a Snapshot.\n                        </p>\n                    </template>\n                    <template v-if=\"hasPermission('device:snapshot:create')\" #actions>\n                        <ff-button\n                            v-if=\"hasPermission('snapshot:import')\"\n                            kind=\"secondary\" :disabled=\"busy || !features.deviceEditor || device.ownerType !== 'application'\"\n                            data-action=\"import-snapshot\"\n                            @click=\"$emit('show-import-snapshot-dialog')\"\n                        >\n                            <template #icon-left><UploadIcon /></template>Upload Snapshot\n                        </ff-button>\n                        <ff-button\n                            kind=\"primary\"\n                            :disabled=\"!canCreateSnapshot\"\n                            data-action=\"create-snapshot\"\n                            @click=\"$emit('show-create-snapshot-dialog')\"\n                        >\n                            <template #icon-left><PlusSmIcon /></template>Create Snapshot\n                        </ff-button>\n                    </template>\n                </EmptyState>\n            </template>\n\n            <SnapshotExportDialog ref=\"snapshotExportDialog\" data-el=\"dialog-export-snapshot\" />\n            <SnapshotEditDialog ref=\"snapshotEditDialog\" data-el=\"dialog-edit-snapshot\" @snapshot-updated=\"onSnapshotEdit\" />\n            <AssetDetailDialog ref=\"snapshotViewerDialog\" data-el=\"dialog-view-snapshot\" />\n            <AssetCompareDialog ref=\"snapshotCompareDialog\" data-el=\"dialog-compare-snapshot\" />\n        </div>\n    </div>\n</template>\n\n<script>\nimport { FilterIcon, PlusSmIcon, UploadIcon } from '@heroicons/vue/outline'\nimport { markRaw } from 'vue'\nimport { mapState } from 'vuex'\n\nimport ApplicationApi from '../../../../api/application.js'\nimport DeviceApi from '../../../../api/devices.js'\nimport SnapshotApi from '../../../../api/snapshots.js'\nimport DropdownMenu from '../../../../components/DropdownMenu.vue'\n\nimport EmptyState from '../../../../components/EmptyState.vue'\nimport AssetCompareDialog from '../../../../components/dialogs/AssetCompareDialog.vue'\nimport AssetDetailDialog from '../../../../components/dialogs/AssetDetailDialog.vue'\nimport SnapshotEditDialog from '../../../../components/dialogs/SnapshotEditDialog.vue'\nimport UserCell from '../../../../components/tables/cells/UserCell.vue'\nimport { downloadData } from '../../../../composables/Download.js'\nimport permissionsMixin from '../../../../mixins/Permissions.js'\nimport Alerts from '../../../../services/alerts.js'\nimport Dialog from '../../../../services/dialog.js'\nimport { applySystemUserDetails } from '../../../../transformers/snapshots.transformer.js'\nimport { isAutoSnapshot } from '../../../../utils/snapshot.js'\nimport DaysSince from '../../../application/Snapshots/components/cells/DaysSince.vue'\nimport SnapshotName from '../../../application/Snapshots/components/cells/SnapshotName.vue'\nimport SnapshotSource from '../../../application/Snapshots/components/cells/SnapshotSource.vue'\nimport SnapshotExportDialog from '../../../application/Snapshots/components/dialogs/SnapshotExportDialog.vue'\n\nexport default {\n    name: 'DeviceSnapshots',\n    components: {\n        AssetDetailDialog,\n        AssetCompareDialog,\n        DropdownMenu,\n        EmptyState,\n        FilterIcon,\n        PlusSmIcon,\n        SnapshotEditDialog,\n        SnapshotExportDialog,\n        UploadIcon\n    },\n    mixins: [permissionsMixin],\n    inheritAttrs: false,\n    props: {\n        device: {\n            type: Object,\n            required: true\n        },\n        showDeviceSnapshotsOnly: {\n            type: Boolean,\n            required: false,\n            default: false\n        },\n        reloadHooks: {\n            type: Array,\n            required: true,\n            default: () => []\n        }\n    },\n    emits: ['device-updated', 'show-import-snapshot-dialog', 'show-create-snapshot-dialog'],\n    data () {\n        return {\n            loading: false,\n            deviceCounts: {},\n            snapshots: [],\n            busyMakingSnapshot: false,\n            busyImportingSnapshot: false,\n            snapshotFilter: null,\n            snapshotFilters: {\n                All_Snapshots: {\n                    name: 'All Snapshots',\n                    selected: true,\n                    filter: null,\n                    action: () => {\n                        this.snapshotFilters.All_Snapshots.selected = true\n                        this.snapshotFilters.User_Snapshots.selected = false\n                        this.snapshotFilters.Auto_Snapshots.selected = false\n                        this.snapshotFilter = this.snapshotFilters.All_Snapshots\n                    }\n                },\n                User_Snapshots: {\n                    name: 'User Snapshots',\n                    selected: false,\n                    filter: (s) => !isAutoSnapshot(s),\n                    action: () => {\n                        this.snapshotFilters.All_Snapshots.selected = false\n                        this.snapshotFilters.User_Snapshots.selected = true\n                        this.snapshotFilters.Auto_Snapshots.selected = false\n                        this.snapshotFilter = this.snapshotFilters.User_Snapshots\n                    }\n                },\n                Auto_Snapshots: {\n                    name: 'Auto Snapshots',\n                    selected: false,\n                    filter: (s) => isAutoSnapshot(s),\n                    action: () => {\n                        this.snapshotFilters.All_Snapshots.selected = false\n                        this.snapshotFilters.User_Snapshots.selected = false\n                        this.snapshotFilters.Auto_Snapshots.selected = true\n                        this.snapshotFilter = this.snapshotFilters.Auto_Snapshots\n                    }\n                }\n            }\n        }\n    },\n    computed: {\n        ...mapState('account', ['teamMembership', 'features']),\n        canCreateSnapshot () {\n            if (!this.developerMode || this.busy) {\n                return false\n            }\n            return this.isOwnedByAnInstance || this.isOwnedByAnApplication\n        },\n        columns () {\n            const cols = [\n                {\n                    label: 'Snapshot',\n                    class: ['w-56 sm:w-48'],\n                    component: {\n                        is: markRaw(SnapshotName),\n                        extraProps: {\n                            // targetSnapshot: this.instance.deviceSettings?.targetSnapshot\n                        }\n                    }\n                },\n                {\n                    label: 'Source',\n                    class: ['w-56'],\n                    key: '_ownerSortKey',\n                    // sortable: !this.moreThanOnePage,\n                    component: {\n                        is: markRaw(SnapshotSource)\n                    }\n                },\n                {\n                    label: 'Node-RED version',\n                    class: ['w-56'],\n                    key: 'modules.node-red'\n                },\n                {\n                    label: 'Created By',\n                    class: ['w-48 hidden md:table-cell'],\n                    component: {\n                        is: markRaw(UserCell),\n                        map: {\n                            avatar: 'user.avatar',\n                            name: 'user.name',\n                            username: 'user.username'\n                        }\n                    }\n                },\n                {\n                    label: 'Date Created',\n                    class: ['w-48 hidden sm:table-cell'],\n                    component: { is: markRaw(DaysSince), map: { date: 'createdAt' } }\n                }\n            ]\n            return cols\n        },\n        snapshotList () {\n            return this.snapshots.map(s => {\n                return {\n                    label: s.name,\n                    description: s.description || '',\n                    value: s.id\n                }\n            })\n        },\n        snapshotsFiltered () {\n            if (this.snapshotFilter?.filter) {\n                return this.snapshots.filter(this.snapshotFilter.filter)\n            }\n            return this.snapshots\n        },\n        snapshotFilterOptions () {\n            return Object.values(this.snapshotFilters)\n        },\n        busy () {\n            return this.busyMakingSnapshot || this.busyImportingSnapshot\n        },\n        developerMode () {\n            return this.device?.mode === 'developer'\n        },\n        isOwnedByAnInstance () {\n            return this.device?.ownerType === 'instance'\n        },\n        isOwnedByAnApplication () {\n            return this.device?.ownerType === 'application'\n        },\n        isUnassigned () {\n            return this.device?.ownerType === ''\n        }\n    },\n    watch: {\n        team: 'fetchData',\n        device: 'fetchData',\n        showDeviceSnapshotsOnly: 'fetchData',\n        reloadHooks: {\n            handler: 'fetchData',\n            deep: true\n        }\n    },\n    mounted () {\n        this.fetchData()\n    },\n    methods: {\n        rowIsThisDevice: function (snapshot) {\n            if (!snapshot || !this.device.id) {\n                return false\n            }\n            return snapshot.device?.id === this.device.id\n        },\n        fetchData: async function () {\n            if (!this.features.deviceEditor || this.isOwnedByAnInstance || this.isUnassigned) {\n                return\n            }\n            if (this.device.id && this.device.application) {\n                this.loading = true\n                const ssFilter = {\n                    deviceId: this.showDeviceSnapshotsOnly ? this.device.id : null\n                }\n                const data = await ApplicationApi.getSnapshots(this.device.application.id, null, null, ssFilter) // TODO Move devices snapshots?\n\n                this.snapshots = data.snapshots.map(snapshot => {\n                    const ownerKey = this.getSortKeyForSnapshotSource(snapshot)\n                    return {\n                        ...snapshot,\n                        ...(ownerKey ? { _ownerSortKey: ownerKey } : { _ownerSortKey: undefined })\n                    }\n                })\n                this.snapshots = applySystemUserDetails(data.snapshots)\n                this.loading = false\n            }\n        },\n        // snapshot actions - delete\n        showDeleteSnapshotDialog (snapshot) {\n            Dialog.show({\n                header: 'Delete Snapshot',\n                text: 'Are you sure you want to delete this snapshot?',\n                kind: 'danger',\n                confirmLabel: 'Delete'\n            }, async () => {\n                await SnapshotApi.deleteSnapshot(snapshot.id)\n                const index = this.snapshots.indexOf(snapshot)\n                this.snapshots.splice(index, 1)\n                Alerts.emit('Successfully deleted snapshot.', 'confirmation')\n            })\n        },\n        async downloadSnapshotPackage (snapshot) {\n            const ss = await SnapshotApi.getSummary(snapshot.id)\n            const owner = ss.device || ss.project\n            const ownerType = ss.device ? 'device' : 'instance'\n            const packageJSON = {\n                name: `${owner.safeName || owner.name}`.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase(),\n                description: `${ownerType} snapshot, ${snapshot.name} - ${snapshot.description}`,\n                private: true,\n                version: '0.0.0-' + snapshot.id,\n                dependencies: ss.modules || {}\n            }\n            downloadData(packageJSON, 'package.json')\n        },\n        getSortKeyForSnapshotSource (snapshot) {\n            if (snapshot.ownerType === 'device') {\n                return 'Device:' + snapshot.device?.name || 'No Name'\n            }\n\n            if (snapshot.ownerType === 'instance') {\n                return 'Instance:' + snapshot.instance?.name || 'No Name'\n            }\n\n            return 'Unassigned'\n        },\n        deploySnapshot (snapshotId) {\n            const snapshot = this.snapshots.find(s => s.id === snapshotId)\n            if (!snapshot) {\n                console.warn('Could not find snapshot to deploy', snapshotId, this.snapshots, this.device)\n                Alerts.emit('Oops, something went wrong! Please refresh the page and try again.', 'warning', 7500)\n                return\n            }\n            const currentTargetSnapshot = this.device.targetSnapshot?.id\n            if (typeof currentTargetSnapshot === 'string' && currentTargetSnapshot === snapshot.id) {\n                Alerts.emit('This snapshot is already deployed to this device.', 'info', 7500)\n                return\n            }\n            let body = `Are you sure you want to restore snapshot '${snapshot.name}' to this device?`\n            if (snapshot.device?.id !== this.device.id) {\n                body = `Snapshot '${snapshot.name}' was not generated by this device. Are you sure you want to deploy it to this device?`\n            }\n\n            Dialog.show({\n                header: `Restore Snapshot to device '${this.device.name}'`,\n                kind: 'danger',\n                text: body,\n                confirmLabel: 'Confirm'\n            }, async () => {\n                try {\n                    await DeviceApi.setSnapshotAsTarget(this.device.id, snapshot.id)\n                    Alerts.emit('Successfully applied the snapshot.', 'confirmation')\n                } catch (err) {\n                    Alerts.emit('Failed to apply snapshot: ' + err.toString(), 'warning', 7500)\n                }\n            })\n        },\n        showViewSnapshotDialog (snapshot) {\n            SnapshotApi.getFullSnapshot(snapshot.id).then((data) => {\n                this.$refs.snapshotViewerDialog.show(data)\n            }).catch(err => {\n                console.error(err)\n                Alerts.emit('Failed to get snapshot.', 'warning')\n            })\n        },\n        showCompareSnapshotDialog (snapshot) {\n            SnapshotApi.getFullSnapshot(snapshot.id)\n                .then((data) => this.$refs.snapshotCompareDialog.show(data, this.snapshotList))\n                .catch(err => {\n                    console.error(err)\n                    Alerts.emit('Failed to get snapshot.', 'warning')\n                })\n        },\n        showDownloadSnapshotDialog (snapshot) {\n            this.$refs.snapshotExportDialog.show(snapshot)\n        },\n        showDeploySnapshotDialog (snapshot) {\n            this.deploySnapshot(snapshot.id)\n        },\n        showEditSnapshotDialog (snapshot) {\n            this.$refs.snapshotEditDialog.show(snapshot)\n        },\n        onSnapshotEdit (snapshot) {\n            const index = this.snapshots.findIndex(s => s.id === snapshot.id)\n            if (index >= 0) {\n                this.snapshots[index].name = snapshot.name\n                this.snapshots[index].description = snapshot.description\n            }\n        },\n        // enable/disable snapshot actions\n        canDeploy (_row) {\n            return !this.developerMode && this.hasPermission('device:snapshot:set-target')\n        },\n        canDownload (_row) {\n            return this.hasPermission('snapshot:export')\n        },\n        canDelete (row) {\n            if (this.rowIsThisDevice(row)) {\n                return this.hasPermission('device:snapshot:delete')\n            }\n            return false // only permit deletion of snapshots created by this device\n        }\n    }\n}\n</script>\n\n<style>\n\ntbody .ff-data-table--row > .ff-data-table--cell > .deploy-this-snapshot-button {\n    display: none;\n}\n\ntbody tr.ff-data-table--row:hover .ff-data-table--cell .deploy-this-snapshot-button {\n    display: flex;\n}\n\n</style>\n","<template>\n    <main v-if=\"device\" class=\"ff-with-status-header\">\n        <Teleport v-if=\"mounted\" to=\"#platform-banner\">\n            <SubscriptionExpiredBanner :team=\"team\" />\n            <TeamTrialBanner v-if=\"team.billing?.trial\" :team=\"team\" />\n        </Teleport>\n        <SectionNavigationHeader :tabs=\"navigation\">\n            <template #breadcrumbs>\n                <ff-nav-breadcrumb :to=\"{name: 'TeamDevices', params: {team_slug: team.slug}}\">Remote Instances</ff-nav-breadcrumb>\n                <ff-nav-breadcrumb>{{ device.name }}</ff-nav-breadcrumb>\n            </template>\n            <template #status>\n                <div class=\"flex flex-wrap gap-2\">\n                    <DeviceLastSeenBadge :last-seen-at=\"device.lastSeenAt\" :last-seen-ms=\"device.lastSeenMs\" :last-seen-since=\"device.lastSeenSince\" />\n                    <StatusBadge :status=\"device.status\" :instanceId=\"device.id\" instanceType=\"device\" />\n                    <DeviceModeBadge v-if=\"isDevModeAvailable \" :mode=\"device.mode\" />\n                </div>\n            </template>\n            <template #context>\n                <div v-if=\"device?.ownerType === 'application' && device.application\" data-el=\"device-assigned-application\">\n                    Application:\n                    <ff-team-link :to=\"{name: 'Application', params: {id: device.application?.id}}\" class=\"text-blue-600 cursor-pointer hover:text-blue-700 hover:underline\">{{ device.application?.name }}</ff-team-link>\n                </div>\n                <div v-else-if=\"device?.ownerType === 'instance' && device.instance\" data-el=\"device-assigned-instance\">\n                    Instance:\n                    <ff-team-link :to=\"{name: 'Instance', params: {id: device.instance.id}}\" class=\"text-blue-600 cursor-pointer hover:text-blue-700 hover:underline\">{{ device.instance.name }}</ff-team-link>\n                </div>\n                <div v-else data-el=\"device-assigned-none\">\n                    <span class=\"italic\">No Application or Instance Assigned</span> - <a class=\"ff-link\" data-action=\"assign-device\" @click=\"openAssignmentDialog\">Assign</a>\n                </div>\n            </template>\n            <template #tools>\n                <!--\n                    div style 34px is a workaround to prevent the Device Editor button growing taller than adjacent\n                    button (size difference is caused by odd padding in the toggle button, which though not visible\n                    is still there and affects the button height in this div group)\n                -->\n                <div class=\"space-x-2 flex align-center\" style=\"height: 34px;\">\n                    <template v-if=\"isDevModeAvailable\">\n                        <DeveloperModeToggle data-el=\"device-devmode-toggle\" :device=\"device\" :disabled=\"disableModeToggle\" :disabledReason=\"disableModeToggleReason\" @mode-change=\"setDeviceMode\" />\n                        <button v-if=\"!isVisitingAdmin\" v-ff-tooltip:left=\"!editorAvailable ? 'You can edit flows directly when Developer Mode is enabled, and your Edge Instance is connected.' : 'Open Edge Instance Editor'\" data-action=\"open-editor\" class=\"ff-btn transition-fade--color ff-btn--secondary ff-btn-icon h-9\" :disabled=\"!editorAvailable\" @click=\"openTunnel(true)\">\n                            Open Editor\n                            <span class=\"ff-btn--icon ff-btn--icon-right\">\n                                <ExternalLinkIcon />\n                            </span>\n                        </button>\n                    </template>\n                    <FinishSetupButton v-if=\"neverConnected\" :device=\"device\" />\n                    <DropdownMenu v-if=\"hasPermission('device:change-status') && actionsDropdownOptions.length\" data-el=\"device-actions-dropdown\" buttonClass=\"ff-btn ff-btn--primary\" :options=\"actionsDropdownOptions\">Actions</DropdownMenu>\n                </div>\n            </template>\n        </SectionNavigationHeader>\n        <div class=\"mt-4 sm:mt-8\">\n            <Teleport v-if=\"mounted && isVisitingAdmin\" to=\"#platform-banner\">\n                <div class=\"ff-banner\" data-el=\"banner-device-as-admin\">You are viewing this device as an Administrator</div>\n            </Teleport>\n            <div class=\"px-3 pb-3 md:px-6 md:pb-6\">\n                <router-view :instance=\"device.instance\" :closingTunnel=\"closingTunnel\" :openingTunnel=\"openingTunnel\" :device=\"device\" @device-updated=\"loadDevice\" @close-tunnel=\"closeTunnel\" @open-tunnel=\"openTunnel\" @device-refresh=\"deviceRefresh\" @assign-device=\"openAssignmentDialog\" />\n            </div>\n        </div>\n        <!-- Dialogs -->\n        <!-- device tunnel connecting -->\n        <ff-dialog ref=\"dialog\" data-el=\"establish-device-tunnel-dialog\" header=\"Preparing the connection...\">\n            <template #default>\n                <div class=\"flex flex-col ml-6 mr-6\">\n                    <div class=\"mb-4\">\n                        <p>Connecting to the device</p>\n                    </div>\n                    <div class=\"flex justify-between items-center\">\n                        <div class=\"flex text-center\">\n                            <img class=\"h-16 w-16\" src=\"../../images/pictograms/cloud_teal.png\">\n                        </div>\n                        <div class=\"flex-grow m-4\">\n                            <div class=\"w-full\">\n                                <div class=\"h-1 w-full bg-teal-200 overflow-hidden\">\n                                    <div kind=\"secondary\" class=\"progress w-full h-full bg-teal-800 left-right\" />\n                                </div>\n                            </div>\n                        </div>\n                        <div class=\"flex text-center\">\n                            <img class=\"h-16 w-16\" src=\"../../images/pictograms/devices_red.png\">\n                        </div>\n                    </div>\n                </div>\n            </template>\n            <template #actions>\n                <ff-button data-action=\"tunnel-connect-cancel\" kind=\"secondary\" class=\"ml-4\" @click=\"closeTunnel()\">Cancel</ff-button>\n            </template>\n        </ff-dialog>\n        <AssignDeviceDialog\n            v-if=\"notAssigned\"\n            ref=\"assignment-dialog\"\n            data-el=\"assignment-dialog\"\n            @assign-option-selected=\"assignOptionSelected\"\n        />\n        <DeviceAssignInstanceDialog\n            v-if=\"notAssigned\"\n            ref=\"deviceAssignInstanceDialog\"\n            data-el=\"assignment-dialog-instance\"\n            @assign-device=\"assignDeviceToInstance\"\n        />\n        <DeviceAssignApplicationDialog\n            v-if=\"notAssigned\"\n            ref=\"deviceAssignApplicationDialog\"\n            data-el=\"assignment-dialog-application\"\n            @assign-device=\"assignDeviceToApplication\"\n        />\n    </main>\n</template>\n\n<script>\n\nimport { ExternalLinkIcon } from '@heroicons/vue/outline'\nimport { TerminalIcon } from '@heroicons/vue/solid'\nimport semver from 'semver'\nimport { mapState } from 'vuex'\n\nimport deviceApi from '../../api/devices.js'\nimport DropdownMenu from '../../components/DropdownMenu.vue'\nimport FinishSetupButton from '../../components/FinishSetup.vue'\nimport SectionNavigationHeader from '../../components/SectionNavigationHeader.vue'\nimport StatusBadge from '../../components/StatusBadge.vue'\nimport SubscriptionExpiredBanner from '../../components/banners/SubscriptionExpired.vue'\nimport TeamTrialBanner from '../../components/banners/TeamTrial.vue'\nimport deviceActionsMixin from '../../mixins/DeviceActions.js'\nimport permissionsMixin from '../../mixins/Permissions.js'\n\nimport Alerts from '../../services/alerts.js'\nimport Dialog from '../../services/dialog.js'\nimport { DeviceStateMutator } from '../../utils/DeviceStateMutator.js'\nimport { Roles } from '../../utils/roles.js'\nimport { createPollTimer } from '../../utils/timers.js'\n\nimport DeviceAssignApplicationDialog from '../team/Devices/dialogs/DeviceAssignApplicationDialog.vue'\nimport DeviceAssignInstanceDialog from '../team/Devices/dialogs/DeviceAssignInstanceDialog.vue'\n\nimport AssignDeviceDialog from './components/AssignDeviceDialog.vue'\n\nimport DeveloperModeToggle from './components/DeveloperModeToggle.vue'\nimport DeviceLastSeenBadge from './components/DeviceLastSeenBadge.vue'\nimport DeviceModeBadge from './components/DeviceModeBadge.vue'\n\n// constants\nconst POLL_TIME = 5000\n\nconst deviceTransitionStates = [\n    'loading',\n    'installing',\n    'starting',\n    'stopping',\n    'restarting',\n    'suspending',\n    'importing'\n]\n\nexport default {\n    name: 'DevicePage',\n    components: {\n        FinishSetupButton,\n        ExternalLinkIcon,\n        DeveloperModeToggle,\n        DeviceModeBadge,\n        DeviceLastSeenBadge,\n        DropdownMenu,\n        SectionNavigationHeader,\n        StatusBadge,\n        SubscriptionExpiredBanner,\n        TeamTrialBanner,\n        AssignDeviceDialog,\n        DeviceAssignApplicationDialog,\n        DeviceAssignInstanceDialog\n    },\n    mixins: [permissionsMixin, deviceActionsMixin],\n    data: function () {\n        return {\n            mounted: false,\n            device: null,\n            agentSupportsDeviceAccess: false,\n            agentSupportsActions: false,\n            openingTunnel: false,\n            closingTunnel: false,\n            /** @type {import('../../utils/timers.js').PollTimer} */\n            pollTimer: null,\n            /** @type {DeviceStateMutator} */\n            deviceStateMutator: null\n        }\n    },\n    computed: {\n        ...mapState('account', ['teamMembership', 'team', 'features', 'settings']),\n        isVisitingAdmin: function () {\n            return this.teamMembership.role === Roles.Admin\n        },\n        isMember: function () {\n            return this.teamMembership.role === Roles.Member || this.teamMembership.role === Roles.Owner\n        },\n        isDevModeAvailable: function () {\n            return !!this.features.deviceEditor\n        },\n        developerMode: function () {\n            return this.device && this.agentSupportsDeviceAccess && this.device.mode === 'developer'\n        },\n        deviceRunning () {\n            return this.device?.status === 'running'\n        },\n        disableModeToggle: function () {\n            return !this.isDevModeAvailable ||\n                !this.device ||\n                !this.agentSupportsDeviceAccess ||\n                !this.isMember\n        },\n        disableModeToggleReason: function () {\n            if (!this.device) {\n                return 'No Device selected'\n            }\n            if (!this.agentSupportsDeviceAccess) {\n                return 'Device Agent V0.8 or greater is required'\n            }\n            if (!this.isMember) {\n                return 'Only an Owner or Member can change the Device Mode'\n            }\n            return undefined\n        },\n        editorAvailable: function () {\n            return this.isDevModeAvailable &&\n                this.device &&\n                this.agentSupportsDeviceAccess &&\n                this.developerMode &&\n                this.device.status === 'running'\n        },\n        deviceEditorURL: function () {\n            return this.device.editor?.url || ''\n        },\n        neverConnected () {\n            return !this.device.lastSeenAt\n        },\n        notAssigned () {\n            const device = this.device\n            const hasApplication = device?.ownerType === 'application' && device.application\n            const hasInstance = device?.ownerType === 'instance' && device.instance\n            return !hasApplication && !hasInstance\n        },\n        navigation () {\n            return [\n                { label: 'Overview', to: { name: 'DeviceOverview' }, tag: 'device-overview' },\n                {\n                    label: 'Version History',\n                    to: { name: 'DeviceSnapshots', params: { id: this.$route.params.id } },\n                    tag: 'version-history'\n                },\n                { label: 'Audit Log', to: { name: 'device-audit-log' }, tag: 'device-audit-log' },\n                {\n                    label: 'Node-RED Logs',\n                    to: { name: 'device-logs' },\n                    tag: 'device-logs',\n                    icon: TerminalIcon\n                },\n                { label: 'Settings', to: { name: 'device-settings' }, tag: 'device-settings' },\n                {\n                    label: 'Developer Mode',\n                    to: { name: 'DeviceDeveloperMode' },\n                    tag: 'device-devmode',\n                    hidden: !(this.isDevModeAvailable && this.device.mode === 'developer')\n                }\n            ]\n        },\n        actionsDropdownOptions () {\n            const flowActionsDisabled = !(this.device.status !== 'suspended')\n\n            const deviceStateChanging = this.device.pendingStateChange || this.device.optimisticStateChange\n\n            const result = [\n                // Start and Suspend are disabled until resolution of the feature is complete\n                // See comments in #3292\n                // {\n                //     name: 'Start',\n                //     action: this.startDevice,\n                //     disabled: deviceStateChanging || this.deviceRunning\n                // },\n                // { name: 'Suspend', class: ['text-red-700'], action: this.showConfirmSuspendDialog, disabled: deviceStateChanging || flowActionsDisabled }\n            ]\n\n            if (!this.neverConnected) {\n                // if we've never connected, we know we can't restart\n                result.push({ name: 'Restart', action: this.restartDevice, disabled: deviceStateChanging || flowActionsDisabled })\n            }\n\n            if (this.hasPermission('device:delete')) {\n                result.push(null)\n                result.push({ name: 'Delete', class: ['text-red-700'], action: this.showConfirmDeleteDialog })\n            }\n\n            return result\n        }\n    },\n    watch: {\n        device: 'deviceChanged'\n    },\n    async mounted () {\n        this.mounted = true\n        await this.loadDevice()\n        this.pollTimer = createPollTimer(this.pollTimerElapsed, POLL_TIME)\n    },\n    unmounted () {\n        this.pollTimer?.stop()\n        clearTimeout(this.openTunnelTimeout)\n    },\n    methods: {\n        pollTimerElapsed: async function () {\n            // Only refresh device via the timer if we are on the overview page, developer mode page\n            // the device status is empty or the device is in a transition state\n            // This is to prevent settings pages from refreshing the device state while modifying settings\n            // See `watch: { device: { handler () ...  in pages/device/Settings/General.vue for why that happens\n            const settingsPages = ['DeviceOverview', 'DeviceDeveloperMode']\n            try {\n                if (settingsPages.includes(this.$route.name)) {\n                    await this.loadDevice()\n                } else if (typeof this.device?.status === 'undefined') {\n                    await this.loadDevice()\n                } else if (deviceTransitionStates.includes(this.device?.status)) {\n                    await this.loadDevice()\n                }\n            } catch (err) {\n                if (err.response.status === 404) {\n                    this.pollTimer?.stop()\n                }\n            }\n        },\n        loadDevice: async function () {\n            this.device = await deviceApi.getDevice(this.$route.params.id)\n            if (this.deviceStateMutator) {\n                this.deviceStateMutator.clearState()\n            }\n            this.agentSupportsDeviceAccess = this.device.agentVersion && semver.gte(this.device.agentVersion, '0.8.0')\n            this.agentSupportsActions = this.device.agentVersion && semver.gte(this.device.agentVersion, '2.3.0')\n            this.$store.dispatch('account/setTeam', this.device.team.slug)\n        },\n        deviceRefresh: async function () {\n            if (this.pollTimer.running) {\n                // If the poll timer is running, we don't need to manually refresh the device\n                return\n            }\n            this.loadDevice()\n        },\n        showOpenEditorDialog: async function () {\n            this.$refs['open-editor-dialog'].show()\n        },\n        setDeviceMode: async function (newMode, callback) {\n            try {\n                if (newMode !== 'autonomous' && newMode !== 'developer') {\n                    throw new Error('Unsupported mode')\n                }\n                // call to close tunnel regardless of selected mode being set\n                const disableResult = await deviceApi.disableEditorTunnel(this.device.id)\n                // set the selected mode\n                const setModeResult = await deviceApi.setMode(this.device.id, newMode)\n                // update the device properties to reflect immediate status\n                this.device.editor = {\n                    enabled: !!disableResult?.editor?.enabled,\n                    connected: !!disableResult?.editor?.connected,\n                    url: disableResult?.editor?.url\n                }\n                this.device.mode = setModeResult?.mode\n                callback(null, setModeResult)\n            } catch (error) {\n                if (callback) {\n                    callback(error)\n                } else {\n                    throw new Error('Unknown mode')\n                }\n            }\n        },\n        openAssignmentDialog () {\n            this.$refs['assignment-dialog'].show()\n        },\n        assignOptionSelected (option) {\n            if (option === 'instance') {\n                this.$refs.deviceAssignInstanceDialog.show(this.device)\n            } else if (option === 'application') {\n                this.$refs.deviceAssignApplicationDialog.show(this.device)\n            }\n        },\n        async assignDeviceToInstance (device, instanceId) {\n            this.device = await deviceApi.updateDevice(device.id, { instance: instanceId })\n\n            Alerts.emit('Device successfully assigned to instance.', 'confirmation')\n        },\n\n        async assignDeviceToApplication (device, applicationId) {\n            this.device = await deviceApi.updateDevice(device.id, { application: applicationId, instance: null })\n\n            Alerts.emit('Device successfully assigned to application.', 'confirmation')\n        },\n        openEditor () {\n            this.$store.dispatch('ux/validateUserAction', 'hasOpenedDeviceEditor')\n            window.open(this.deviceEditorURL, `device-editor-${this.device.id}`)\n        },\n        async openTunnel (launchEditor = false) {\n            try {\n                if (this.deviceRunning) {\n                    if (this.device.editor?.enabled && this.device.editor?.connected && this.device.editor?.local) {\n                        this.openEditor()\n                    } else {\n                        this.openingTunnel = true\n                        this.$refs.dialog.show()\n\n                        // Polls the tunnel status until we see it connected to the\n                        // 'local' platform instance - will give up after 10 attempts\n                        const pollTunnelStatus = (done, attempt = 0, timeout = 500) => {\n                            if (attempt < 10) {\n                                this.openTunnelTimeout = setTimeout(async () => {\n                                    await this.loadDevice()\n                                    if (this.device.editor?.enabled && this.device.editor?.connected) {\n                                        if (this.device.editor?.local) {\n                                            if (launchEditor) {\n                                                this.openEditor()\n                                            }\n                                        } else {\n                                            pollTunnelStatus(done, attempt + 1, 200)\n                                            return\n                                        }\n                                    }\n                                    done()\n                                }, timeout)\n                            }\n                        }\n\n                        try {\n                            if (!this.device.editor?.enabled || !this.device.editor?.connected) {\n                                // * Enable Device Editor (Step 1) - (browser->frontendApi) User clicks button to \"Enable Editor\"\n                                const result = await deviceApi.enableEditorTunnel(this.device.id)\n                                this.updateTunnelStatus(result)\n                            }\n                            pollTunnelStatus(() => {\n                                this.$refs.dialog.close()\n                                this.openingTunnel = false\n                            })\n                        } catch (err) {\n                            this.$refs.dialog.close()\n                            this.openingTunnel = false\n                        }\n                    }\n                } else {\n                    Alerts.emit('Unable to establish a connection to the device. Please check it is connected and running then try again', 'warning', 7500)\n                }\n            } catch (err) {\n                console.warn('Error in openTunnel', err)\n            }\n        },\n        async closeTunnel () {\n            this.closingTunnel = true\n            this.$refs.dialog.close()\n            clearTimeout(this.openTunnelTimeout)\n            try {\n                const result = await deviceApi.disableEditorTunnel(this.device.id)\n                this.updateTunnelStatus(result)\n                this.loadDevice(this.loadDevice())\n            } finally {\n                this.closingTunnel = false\n            }\n        },\n        updateTunnelStatus (status) {\n            this.device.editor = this.device.editor || {}\n            this.device.editor.url = status.url\n            this.device.editor.enabled = !!status.enabled\n            this.device.editor.connected = !!status.connected\n        },\n        deviceChanged () {\n            this.deviceStateMutator = new DeviceStateMutator(this.device)\n        },\n        showConfirmDeleteDialog () {\n            Dialog.show({\n                header: 'Delete Device',\n                kind: 'danger',\n                text: 'Are you sure you want to delete this device? Once deleted, there is no going back.',\n                confirmLabel: 'Delete'\n            }, async () => {\n                try {\n                    await deviceApi.deleteDevice(this.device.id)\n                    Alerts.emit('Successfully deleted the device', 'confirmation')\n                    // Trigger a refresh of team info to resync following device changes\n                    await this.$store.dispatch('account/refreshTeam')\n                    this.$router.push({ name: 'TeamDevices', params: { team_slug: this.team.slug } })\n                } catch (err) {\n                    Alerts.emit('Failed to delete device: ' + err.toString(), 'warning', 7500)\n                }\n            })\n        },\n        /**\n         * Checks agent version and shows warning if known old version is present. Returns true if the action can proceed\n         * @param {string} [message] - optional message to show in confirmation dialog. If omitted, no confirmation is shown\n         */\n        preActionChecks (message) {\n            if (this.device.agentVersion && !this.agentSupportsActions) {\n                // if agent version is present but is less than required version, show warning and halt\n                Alerts.emit('Device Agent V2.3 or greater is required to perform this action.', 'warning')\n                return false\n            }\n            if (!message) {\n                // no message means silent operation, no need to show confirmation\n                return true\n            }\n            if (!this.device.agentVersion) {\n                // if agent version is missing, be optimistic and give it a go, but show warning\n                Alerts.emit(`${message}.  NOTE: The device agent version is not known, the action may timeout`, 'warning')\n            } else {\n                Alerts.emit(message, 'confirmation')\n            }\n            return true\n        },\n        async startDevice () {\n            const preCheckOk = this.preActionChecks('Starting device...')\n            if (!preCheckOk) {\n                return\n            }\n            this.deviceStateMutator.setStateOptimistically('starting')\n            try {\n                await deviceApi.startDevice(this.device)\n                this.deviceStateMutator.setStateAsPendingFromServer()\n            } catch (err) {\n                let message = 'Device start request failed.'\n                if (err.response?.data?.error) {\n                    message = err.response.data.error\n                }\n                console.warn(message, err)\n                Alerts.emit(message, 'warning')\n                this.deviceStateMutator.restoreState()\n            }\n        },\n        async restartDevice () {\n            const preCheckOk = this.preActionChecks('Restarting device...')\n            if (!preCheckOk) {\n                return\n            }\n            this.deviceStateMutator.setStateOptimistically('restarting')\n            try {\n                await deviceApi.restartDevice(this.device)\n                this.deviceStateMutator.setStateAsPendingFromServer()\n            } catch (err) {\n                let message = 'Device restart request failed.'\n                if (err.response?.data?.error) {\n                    message = err.response.data.error\n                }\n                console.warn(message, err)\n                Alerts.emit(message, 'warning')\n            }\n        },\n        showConfirmSuspendDialog () {\n            const preCheckOk = this.preActionChecks() // silent check\n            if (!preCheckOk) {\n                return\n            }\n            Dialog.show({\n                header: 'Suspend Device',\n                text: 'Are you sure you want to suspend this device?',\n                confirmLabel: 'Suspend',\n                kind: 'danger'\n            }, () => {\n                this.deviceStateMutator.setStateOptimistically('suspending')\n                deviceApi.suspendDevice(this.device).then(() => {\n                    this.deviceStateMutator.setStateAsPendingFromServer()\n                    Alerts.emit('Device suspend request succeeded.', 'confirmation')\n                }).catch(err => {\n                    let message = 'Device suspend request failed.'\n                    if (err.response?.data?.error) {\n                        message = err.response.data.error\n                    }\n                    console.warn(message, err)\n                    Alerts.emit(message, 'warning')\n                })\n            })\n        }\n    }\n}\n</script>\n\n<style scoped>\n.progress {\n    animation: progress 1s infinite linear;\n}\n\n.left-right {\n    transform-origin: 0% 50%;\n}\n    @keyframes progress {\n    0% {\n        transform:  translateX(0) scaleX(0);\n    }\n    40% {\n        transform:  translateX(0) scaleX(0.4);\n    }\n    100% {\n        transform:  translateX(100%) scaleX(0.5);\n    }\n}\n</style>\n","<template>\n    <!-- set mb-14 (~56px) on the form to permit access to kebab actions where hubspot chat covers it -->\n    <div\n        class=\"space-y-2 mb-14\"\n        data-el=\"devices-section\"\n    >\n        <ff-loading\n            v-if=\"loadingStatuses || loadingDevices\"\n            message=\"Loading Remote Instances...\"\n        />\n        <template v-else-if=\"team\">\n            <FeatureUnavailableToTeam v-if=\"teamDeviceLimitReached\" fullMessage=\"You have reached the limit for Remote Instances in this team.\" :class=\"{'mt-0': displayingTeam }\" />\n            <FeatureUnavailableToTeam v-if=\"teamRuntimeLimitReached\" fullMessage=\"You have reached the limit for Instances in this team.\" :class=\"{'mt-0': displayingTeam }\" />\n            <DevicesStatusBar v-if=\"allDeviceStatuses.size > 0\" data-el=\"devicestatus-lastseen\" label=\"Last Seen\" :devices=\"Array.from(allDeviceStatuses.values())\" property=\"lastseen\" :filter=\"filter\" @filter-selected=\"applyFilter\" />\n            <DevicesStatusBar v-if=\"allDeviceStatuses.size > 0\" data-el=\"devicestatus-status\" label=\"Last Known Status\" :devices=\"Array.from(allDeviceStatuses.values())\" property=\"status\" :filter=\"filter\" @filter-selected=\"applyFilter\" />\n            <ff-data-table\n                v-if=\"allDeviceStatuses.size > 0\"\n                data-el=\"devices-browser\"\n                :columns=\"columns\"\n                :rows=\"devicesWithStatuses\"\n                :show-search=\"true\"\n                search-placeholder=\"Search Remote Instances\"\n                :show-load-more=\"moreThanOnePage\"\n                :check-key=\"row => row.id\"\n                :show-row-checkboxes=\"true\"\n                @rows-checked=\"checkedDevices = $event\"\n                @load-more=\"loadMoreDevices\"\n                @update:search=\"updateSearch\"\n                @update:sort=\"updateSort\"\n            >\n                <template #actions>\n                    <DropdownMenu v-if=\"hasPermission('team:device:bulk-delete') || hasPermission('team:device:bulk-edit')\" :disabled=\"!checkedDevices?.length\" data-el=\"bulk-actions-dropdown\" buttonClass=\"ff-btn ff-btn--secondary\" :options=\"bulkActionsDropdownOptions\">Actions</DropdownMenu>\n                    <ff-button\n                        v-if=\"displayingInstance && hasPermission('project:snapshot:create')\"\n                        data-action=\"change-target-snapshot\"\n                        kind=\"secondary\"\n                        @click=\"showSelectTargetSnapshotDialog\"\n                    >\n                        <template #icon-left>\n                            <ClockIcon />\n                        </template>\n                        <span class=\"font-normal\">\n                            Target Snapshot: <b>{{ instance.targetSnapshot?.name || 'none' }}</b>\n                        </span>\n                    </ff-button>\n                    <ff-button\n                        v-if=\"hasPermission('device:create')\"\n                        class=\"font-normal\"\n                        data-action=\"register-device\"\n                        kind=\"primary\"\n                        :disabled=\"teamDeviceLimitReached || teamRuntimeLimitReached\"\n                        @click=\"showCreateDeviceDialog\"\n                    >\n                        <template #icon-left>\n                            <PlusSmIcon />\n                        </template>\n                        Add Remote Instance\n                    </ff-button>\n                </template>\n                <template\n                    v-if=\"hasPermission('device:edit')\"\n                    #context-menu=\"{row}\"\n                >\n                    <ff-list-item\n                        label=\"Edit Details\"\n                        @click=\"deviceAction('edit', row.id)\"\n                    />\n                    <ff-list-item\n                        v-if=\"!row.ownerType && displayingTeam\"\n                        label=\"Add to Application\"\n                        data-action=\"device-assign-to-application\"\n                        @click=\"deviceAction('assignToApplication', row.id)\"\n                    />\n                    <ff-list-item\n                        v-else-if=\"row.ownerType === 'application' && (displayingTeam || displayingApplication)\"\n                        label=\"Remove from Application\"\n                        data-action=\"device-remove-from-application\"\n                        @click=\"deviceAction('removeFromApplication', row.id)\"\n                    />\n                    <ff-list-item\n                        v-if=\"!row.ownerType && displayingTeam\"\n                        label=\"Add to Instance\"\n                        data-action=\"device-assign-to-instance\"\n                        @click=\"deviceAction('assignToProject', row.id)\"\n                    />\n                    <ff-list-item\n                        v-else-if=\"row.ownerType === 'instance' && (displayingTeam || displayingInstance)\"\n                        label=\"Remove from Instance\"\n                        data-action=\"device-remove-from-instance\"\n                        @click=\"deviceAction('removeFromProject', row.id)\"\n                    />\n                    <ff-list-item\n                        kind=\"danger\"\n                        label=\"Regenerate Configuration\"\n                        @click=\"deviceAction('updateCredentials', row.id)\"\n                    />\n                    <ff-list-item\n                        v-if=\"hasPermission('device:delete')\"\n                        kind=\"danger\"\n                        label=\"Delete Device\"\n                        @click=\"deviceAction('delete', row.id)\"\n                    />\n                </template>\n            </ff-data-table>\n            <template v-else>\n                <template v-if=\"displayingTeam\">\n                    <EmptyState data-el=\"team-no-devices\">\n                        <template #img>\n                            <img src=\"../images/empty-states/team-devices.png\">\n                        </template>\n                        <template #header>Connect your First Remote Instance</template>\n                        <template #message>\n                            <p>\n                                FlowFuse allow you to manage Node-RED instances\n                                running on remote hardware.\n                            </p>\n                            <p>\n                                To manage your  <a\n                                    class=\"ff-link\" href=\"https://flowfuse.com/docs/user/devices\"\n                                    target=\"_blank\"\n                                >FlowFuse Device Agent</a>, and can be used to deploy and debug\n                                instances anywhere, from here, in FlowFuse.\n                            </p>\n                        </template>\n                        <template #actions>\n                            <ff-button\n                                v-if=\"hasPermission('device:create')\"\n                                class=\"font-normal\"\n                                kind=\"primary\"\n                                :disabled=\"teamDeviceLimitReached || teamRuntimeLimitReached\"\n                                data-action=\"register-device\"\n                                @click=\"showCreateDeviceDialog\"\n                            >\n                                <template #icon-left>\n                                    <PlusSmIcon />\n                                </template>\n                                Add Remote Instance\n                            </ff-button>\n                        </template>\n                    </EmptyState>\n                </template>\n                <template v-else-if=\"displayingInstance\">\n                    <EmptyState data-el=\"instance-no-devices\">\n                        <template #img>\n                            <img src=\"../images/empty-states/instance-devices.png\">\n                        </template>\n                        <template #header>Connect your First Remote Instances</template>\n                        <template #message>\n                            <p>\n                                Here, you will see a list of Remote Instances connected to this Hosted Instance.\n                            </p>\n                            <p>\n                                You can deploy <router-link class=\"ff-link\" :to=\"{name: 'instance-snapshots', params: {id: instance.id}}\">Snapshots</router-link> of this Instance to your connected Devices.\n                            </p>\n                            <p>\n                                A full list of your Team's Devices are available <ff-team-link\n                                    class=\"ff-link\"\n                                    :to=\"{name: 'TeamDevices', params: {team_slug: team.slug}}\"\n                                >\n                                    here\n                                </ff-team-link>.\n                            </p>\n                        </template>\n                        <template #actions>\n                            <ff-button\n                                v-if=\"hasPermission('device:create')\"\n                                class=\"font-normal\"\n                                kind=\"primary\"\n                                :disabled=\"teamDeviceLimitReached || teamRuntimeLimitReached\"\n                                data-action=\"register-device\"\n                                @click=\"showCreateDeviceDialog\"\n                            >\n                                <template #icon-left>\n                                    <PlusSmIcon />\n                                </template>\n                                Add Remote Instance\n                            </ff-button>\n                        </template>\n                    </EmptyState>\n                </template>\n                <template v-else-if=\"displayingApplication\">\n                    <EmptyState data-el=\"application-no-devices\">\n                        <template #img>\n                            <img src=\"../images/empty-states/instance-devices.png\">\n                        </template>\n                        <template #header>Connect your First Remote Instance</template>\n                        <template #message>\n                            <p>\n                                Here, you will see a list of Devices belonging to this Application.\n                            </p>\n                            <p>\n                                You can deploy <router-link class=\"ff-link\" :to=\"{name: 'ApplicationSnapshots'}\">Snapshots</router-link> of this Application to your connected Devices.\n                            </p>\n                            <p>\n                                A full list of your Team's Devices are available <ff-team-link\n                                    class=\"ff-link\"\n                                    :to=\"{name: 'TeamDevices', params: {team_slug: team.slug}}\"\n                                >\n                                    here\n                                </ff-team-link>.\n                            </p>\n                        </template>\n                        <template #actions>\n                            <ff-button\n                                v-if=\"hasPermission('device:create')\"\n                                class=\"font-normal\"\n                                kind=\"primary\"\n                                :disabled=\"teamDeviceLimitReached || teamRuntimeLimitReached\"\n                                data-action=\"register-device\"\n                                @click=\"showCreateDeviceDialog\"\n                            >\n                                <template #icon-left>\n                                    <PlusSmIcon />\n                                </template>\n                                Add Remote Instance\n                            </ff-button>\n                        </template>\n                    </EmptyState>\n                </template>\n                <div v-else class=\"ff-no-data ff-no-data-large\">\n                    <span data-el=\"no-devices\">\n                        No Remote Instances found.\n                    </span>\n                </div>\n            </template>\n        </template>\n    </div>\n\n    <TeamDeviceCreateDialog\n        v-if=\"team\"\n        ref=\"teamDeviceCreateDialog\"\n        :team=\"team\"\n        :teamDeviceCount=\"teamDeviceCount\"\n        @device-created=\"deviceCreated\"\n        @device-updated=\"deviceUpdated\"\n    >\n        <template #description>\n            <p v-if=\"!featuresCheck?.isHostedInstancesEnabledForTeam && tours.firstDevice\">\n                Describe your new Remote Instance here, e.g. \"Raspberry Pi\", \"Allen-Bradley PLC\", etc.\n            </p>\n            <p v-else>\n                Remote Instances are managed using the <a href=\"https://flowfuse.com/docs/user/devices/\" target=\"_blank\">FlowFuse Device Agent</a>. The agent will need to be setup on the hardware where you want your Remote Instance to run.\n            </p>\n        </template>\n    </TeamDeviceCreateDialog>\n\n    <DeviceCredentialsDialog ref=\"deviceCredentialsDialog\" />\n\n    <SnapshotAssignDialog\n        v-if=\"displayingInstance\"\n        ref=\"snapshotAssignDialog\"\n        :instance=\"instance\"\n        @snapshot-assigned=\"$emit('instance-updated')\"\n    />\n\n    <DeviceAssignInstanceDialog\n        ref=\"deviceAssignInstanceDialog\"\n        @assign-device=\"assignDevice\"\n        @move-devices=\"moveDevicesToInstance\"\n    />\n\n    <DeviceAssignApplicationDialog\n        ref=\"deviceAssignApplicationDialog\"\n        @assign-device=\"assignDeviceToApplication\"\n        @move-devices=\"moveDevicesToApplication\"\n    />\n\n    <ff-dialog\n        ref=\"teamBulkDeviceDeleteDialog\"\n        header=\"Confirm Device Delete\"\n        class=\"ff-dialog-fixed-height\"\n        confirm-label=\"Confirm\"\n        data-el=\"team-bulk-device-delete-dialog\"\n        kind=\"danger\"\n        @confirm=\"confirmBulkDelete()\"\n    >\n        <template #default>\n            <p>The following device{{ checkedDevices.length > 1 ? 's' : '' }} will be deleted:</p>\n            <div class=\"max-h-96 overflow-y-auto\">\n                <ul class=\"ff-devices-ul\">\n                    <li v-for=\"device in checkedDevices\" :key=\"device.id\">\n                        <span class=\"font-bold\">{{ device.name }}</span> <span class=\"text-gray-500 text-sm\"> ({{ device.id }})</span>\n                    </li>\n                </ul>\n            </div>\n            <p>This action cannot be undone.</p>\n        </template>\n    </ff-dialog>\n\n    <ff-dialog\n        ref=\"devicesMoveNoOwnerDialog\"\n        :header=\"displayingTeam ? `Unassign Device${checkedDevices.length > 1 ? 's' : ''}` : `Remove Device${checkedDevices.length > 1 ? 's' : ''} from ${displayingInstance ? 'Instance' : displayingApplication ? 'Application' : 'Assignment'}`\"\n        class=\"ff-dialog-fixed-height\"\n        :confirm-label=\"displayingTeam ?'Unassign' : 'Remove'\"\n        data-el=\"team-bulk-device-unassign-dialog\"\n        kind=\"danger\"\n        @confirm=\"moveDevicesToUnassigned(checkedDevices)\"\n    >\n        <template #default>\n            <p v-if=\"displayingInstance\">The following devices will be removed from this Instance:</p>\n            <p v-else-if=\"displayingApplication\">The following devices will be removed from this Application:</p>\n            <p v-else>The following devices will be removed from their current assignment:</p>\n            <div class=\"max-h-96 overflow-y-auto\">\n                <ul class=\"ff-devices-ul\">\n                    <li v-for=\"device in checkedDevices\" :key=\"device.id\">\n                        <span class=\"font-bold\">{{ device.name }}</span> <span class=\"text-gray-500 text-sm\"> ({{ device.id }})</span>\n                    </li>\n                </ul>\n            </div>\n            <p>This will stop the flows running on the device{{ checkedDevices.length > 1 ? 's' : '' }}.</p>\n        </template>\n    </ff-dialog>\n</template>\n\n<script>\nimport { ClockIcon } from '@heroicons/vue/outline'\nimport { PlusSmIcon } from '@heroicons/vue/solid'\n\nimport { markRaw } from 'vue'\nimport { mapGetters, mapState } from 'vuex'\n\nimport deviceApi from '../api/devices.js'\nimport teamApi from '../api/team.js'\nimport DropdownMenu from '../components/DropdownMenu.vue'\nimport deviceActionsMixin from '../mixins/DeviceActions.js'\nimport permissionsMixin from '../mixins/Permissions.js'\n\nimport DeviceAssignedToLink from '../pages/application/components/cells/DeviceAssignedToLink.vue'\nimport DeviceLink from '../pages/application/components/cells/DeviceLink.vue'\nimport Snapshot from '../pages/application/components/cells/Snapshot.vue'\n\nimport DeviceLastSeenCell from '../pages/device/components/DeviceLastSeenCell.vue'\nimport SnapshotAssignDialog from '../pages/instance/VersionHistory/Snapshots/dialogs/SnapshotAssignDialog.vue'\nimport InstanceStatusBadge from '../pages/instance/components/InstanceStatusBadge.vue'\nimport DeviceAssignApplicationDialog from '../pages/team/Devices/dialogs/DeviceAssignApplicationDialog.vue'\nimport DeviceAssignInstanceDialog from '../pages/team/Devices/dialogs/DeviceAssignInstanceDialog.vue'\nimport DeviceCredentialsDialog from '../pages/team/Devices/dialogs/DeviceCredentialsDialog.vue'\nimport TeamDeviceCreateDialog from '../pages/team/Devices/dialogs/TeamDeviceCreateDialog.vue'\n\nimport Alerts from '../services/alerts.js'\n\nimport { debounce } from '../utils/eventHandling.js'\nimport { createPollTimer } from '../utils/timers.js'\n\nimport EmptyState from './EmptyState.vue'\nimport FeatureUnavailableToTeam from './banners/FeatureUnavailableToTeam.vue'\nimport DevicesStatusBar from './charts/DeviceStatusBar.vue'\n\nconst POLL_TIME = 10000\n\nexport default {\n    name: 'DevicesBrowser',\n    components: {\n        ClockIcon,\n        DeviceAssignApplicationDialog,\n        DeviceAssignInstanceDialog,\n        DeviceCredentialsDialog,\n        DropdownMenu,\n        FeatureUnavailableToTeam,\n        PlusSmIcon,\n        SnapshotAssignDialog,\n        TeamDeviceCreateDialog,\n        EmptyState,\n        DevicesStatusBar\n    },\n    mixins: [permissionsMixin, deviceActionsMixin],\n    inheritAttrs: false,\n    props: {\n        // One of the two must be provided\n        instance: {\n            type: Object,\n            required: false,\n            default: null\n        },\n        application: {\n            type: Object,\n            required: false,\n            default: null\n        }\n    },\n    emits: ['instance-updated'],\n    data () {\n        return {\n            // Page state\n            loadingStatuses: true,\n            loadingDevices: true,\n            creatingDevice: false,\n            deletingDevice: false,\n\n            // Devices lists\n            devices: new Map(), // devices currently available to be displayed\n\n            checkedDevices: [], // devices currently selected in the table\n\n            unsearchedHasMoreThanOnePage: true,\n            unfilteredHasMoreThanOnePage: true,\n\n            sort: {\n                key: null,\n                direction: 'desc'\n            },\n            /** @type { import('../utils/timers.js').PollTimer } */\n            pollTimer: null\n        }\n    },\n    computed: {\n        ...mapState('account', ['team', 'teamMembership']),\n        ...mapState('ux/tours', ['tours']),\n        ...mapGetters('account', ['featuresCheck']),\n        columns () {\n            const columns = [\n                { label: 'Remote Instance', key: 'name', sortable: !this.moreThanOnePage, component: { is: markRaw(DeviceLink) } },\n                { label: 'Type', key: 'type', class: ['w-48'], sortable: !this.moreThanOnePage },\n                { label: 'Last Seen', key: 'lastSeenAt', class: ['w-48'], sortable: !this.moreThanOnePage, component: { is: markRaw(DeviceLastSeenCell) } },\n                { label: 'Last Known Status', class: ['w-32'], component: { is: markRaw(InstanceStatusBadge), map: { instanceId: 'id' }, extraProps: { instanceType: 'device' } } }\n            ]\n\n            if (this.displayingTeam) {\n                // Show which application/instance the device is assigned to when looking at devices owned by a team\n                columns.push({\n                    label: 'Assigned To',\n                    class: ['w-48'],\n                    key: '_ownerSortKey',\n                    sortable: !this.moreThanOnePage,\n                    component: {\n                        is: markRaw(DeviceAssignedToLink)\n                    }\n                })\n            } else if (this.displayingInstance) {\n                // Show snapshot info when looking at devices owned by an instance\n                columns.push(\n                    { label: 'Deployed Snapshot', class: ['w-48'], component: { is: markRaw(Snapshot) } }\n                )\n            }\n\n            return columns\n        },\n        filteredDevices () {\n            const devicesToDisplay = new Set(this.filter?.devices)\n\n            return Array.from(this.devices.values()).filter((device) => {\n                if (!this.filter || this.unfilteredHasMoreThanOnePage) {\n                    return true\n                }\n\n                return devicesToDisplay.has(device.id)\n            })\n        },\n        devicesWithStatuses () {\n            const output = this.filteredDevices.map(device => {\n                const statusObject = this.allDeviceStatuses.get(device.id)\n                const ownerKey = this.getOwnerSortKeyForDevice(device)\n\n                return {\n                    ...device,\n                    ...statusObject,\n                    ...(ownerKey ? { _ownerSortKey: ownerKey } : { _ownerSortKey: undefined })\n                }\n            })\n\n            return output\n        },\n        hasLoadedModel () {\n            return (\n                (this.displayingInstance && !!this.instance?.id) ||\n                (this.displayingApplication && !!this.application?.id) ||\n                (this.displayingTeam && !!this.team?.id)\n            )\n        },\n        moreThanOnePage () {\n            return !!this.nextCursor\n        },\n        teamRuntimeLimitReached () {\n            let teamTypeRuntimeLimit = this.team.type.properties?.runtimes?.limit\n            // Uses this.teamDeviceCount as that tracks live updates made in the page\n            // that may not have made it to this.team.deviceCount yet\n            const currentRuntimeCount = this.teamDeviceCount + this.team.instanceCount\n            if (this.team.billing?.trial && !this.team.billing?.active && this.team.type.properties?.trial?.runtimesLimit) {\n                teamTypeRuntimeLimit = this.team.type.properties?.trial?.runtimesLimit\n            }\n            return (teamTypeRuntimeLimit > 0 && currentRuntimeCount >= teamTypeRuntimeLimit)\n        },\n        teamDeviceLimitReached () {\n            const teamTypeDeviceLimit = this.team.type.properties?.devices?.limit\n            if (teamTypeDeviceLimit > 0 && this.teamDeviceCount >= teamTypeDeviceLimit) {\n                // Device specific limit has been reached\n                return true\n            }\n            return false\n        },\n        bulkActionsDropdownOptions () {\n            const actionsEnabled = this.checkedDevices?.length > 0\n            const enableDelete = actionsEnabled && this.hasPermission('team:device:bulk-delete')\n            const enableMove = actionsEnabled && this.hasPermission('team:device:bulk-edit')\n            const showRemoveFromInstance = this.displayingInstance || this.displayingTeam\n            const showRemoveFromApplication = this.displayingApplication || this.displayingTeam\n            const menu = []\n            menu.push({ name: 'Move to Instance', action: this.showTeamBulkDeviceMoveToInstanceDialog, disabled: !enableMove })\n            menu.push({ name: 'Move to Application', action: this.showTeamBulkDeviceMoveToApplicationDialog, disabled: !enableMove })\n            if (this.displayingInstance && showRemoveFromInstance) {\n                menu.push({ name: 'Remove from Instance', action: this.showTeamBulkDeviceUnassignDialog, disabled: !enableMove })\n            } else if (this.displayingApplication && showRemoveFromApplication) {\n                menu.push({ name: 'Remove from Application', action: this.showTeamBulkDeviceUnassignDialog, disabled: !enableMove })\n            } else if (this.displayingTeam && (showRemoveFromInstance || showRemoveFromApplication)) {\n                menu.push({ name: 'Unassign', action: this.showTeamBulkDeviceUnassignDialog, disabled: !enableMove })\n            }\n            menu.push({ name: 'Delete', class: ['!text-red-600'], action: this.showTeamBulkDeviceDeleteDialog, disabled: !enableDelete })\n            return menu\n        }\n    },\n    watch: {\n        instance: 'fullReloadOfData',\n        application: 'fullReloadOfData',\n        team: 'fullReloadOfData'\n    },\n    mounted () {\n        this.fullReloadOfData()\n        this.pollTimer = createPollTimer(this.pollTimerElapsed, POLL_TIME) // auto starts\n    },\n    async unmounted () {\n        this.pollTimer.stop()\n        if (this.deviceCountDeltaSincePageLoad !== 0) {\n            // Trigger a refresh of team info to resync following device\n            // changes\n            await this.$store.dispatch('account/refreshTeam')\n        }\n    },\n    methods: {\n        pollTimerElapsed: async function () {\n            this.pollTimer.pause()\n            try {\n                await this.pollForDeviceStatuses()\n            } finally {\n                this.pollTimer.resume()\n            }\n        },\n\n        /**\n         * filter: Object containing keys:\n         *  - devices: an array of device ids\n         *  - property: which filter row is being applied, e.g. status or lastseen\n         *  - bucket: which value of this property are we filtering on from the buckets in the status bar\n         */\n        applyFilter (filter) {\n            this.filter = filter\n\n            if (this.unfilteredHasMoreThanOnePage) {\n                this.doFilterServerSide()\n            }\n        },\n\n        updateSearch (searchTerm) {\n            this.searchTerm = searchTerm\n\n            if (this.unsearchedHasMoreThanOnePage) {\n                this.doSearchServerSide()\n            }\n        },\n\n        updateSort (key, direction) {\n            this.sort.key = key\n            this.sort.direction = direction\n\n            if (this.moreThanOnePage) {\n                this.doSortServerSide()\n            }\n        },\n\n        doFilterServerSide: debounce(function () {\n            this.loadDevices(true)\n        }, 50),\n\n        doSearchServerSide: debounce(function () {\n            this.loadDevices(true)\n        }, 150),\n\n        doSortServerSide: debounce(function () {\n            this.loadDevices(true)\n        }, 50),\n\n        showCreateDeviceDialog () {\n            const showApplicationsList = this.displayingTeam\n            this.$refs.teamDeviceCreateDialog.show(null, this.instance, this.application, showApplicationsList)\n        },\n\n        confirmBulkDelete () {\n            // do the delete\n            teamApi.bulkDeviceDelete(this.team?.id, this.checkedDevices.map(device => device.id))\n                .then(() => {\n                    Alerts.emit('Devices successfully deleted.', 'confirmation')\n                    this.fullReloadOfData()\n                })\n                .catch((error) => {\n                    Alerts.emit('Error deleting devices: ' + error.message, 'error')\n                })\n        },\n\n        showTeamBulkDeviceDeleteDialog () {\n            this.$refs.teamBulkDeviceDeleteDialog.show()\n        },\n\n        showTeamBulkDeviceUnassignDialog () {\n            this.$refs.devicesMoveNoOwnerDialog.show()\n        },\n\n        showTeamBulkDeviceMoveToInstanceDialog () {\n            this.$refs.deviceAssignInstanceDialog.show(this.checkedDevices)\n        },\n\n        showTeamBulkDeviceMoveToApplicationDialog () {\n            this.$refs.deviceAssignApplicationDialog.show(this.checkedDevices)\n        },\n\n        showSelectTargetSnapshotDialog () {\n            this.$refs.snapshotAssignDialog.show()\n        },\n\n        async assignDevice (device, instanceId) {\n            const updatedDevice = await deviceApi.updateDevice(device.id, { instance: instanceId })\n\n            Alerts.emit('Device successfully assigned to instance.', 'confirmation')\n\n            this.updateLocalCopyOfDevice({ ...device, ...updatedDevice })\n        },\n\n        async assignDeviceToApplication (device, applicationId) {\n            const updatedDevice = await deviceApi.updateDevice(device.id, { application: applicationId, instance: null })\n\n            Alerts.emit('Device successfully assigned to application.', 'confirmation')\n\n            this.updateLocalCopyOfDevice({ ...device, ...updatedDevice })\n        },\n\n        /**\n         * @param {Array<object>} devices - Array of devices to move\n         * @param {string} instance - ID of the instance to move the devices to\n         */\n        async moveDevicesToInstance (devices, instance) {\n            const deviceIds = devices.map(device => device.id)\n            const data = await teamApi.bulkDeviceMove(this.team.id, deviceIds, 'instance', instance)\n            if (data?.devices.length) {\n                Alerts.emit('Devices successfully moved.', 'confirmation')\n                data.devices.forEach(updatedDevice => {\n                    const device = this.devices.get(updatedDevice.id)\n                    // ensure the updated device has `instance` and `application` set so that the local copy is updated correctly\n                    const ensureProps = { instance: updatedDevice.instance || null, application: updatedDevice.application || null }\n                    this.updateLocalCopyOfDevice({ ...device, ...updatedDevice, ...ensureProps })\n                })\n            }\n        },\n\n        /**\n         * @param {Array<object>} devices - Array of devices to move\n         * @param {string} application - ID of the application to move the devices to\n         */\n        async moveDevicesToApplication (devices, application) {\n            const deviceIds = devices.map(device => device.id)\n            const data = await teamApi.bulkDeviceMove(this.team.id, deviceIds, 'application', application)\n            if (data?.devices.length) {\n                Alerts.emit('Devices successfully moved.', 'confirmation')\n                data.devices.forEach(updatedDevice => {\n                    const device = this.devices.get(updatedDevice.id)\n                    // ensure the updated device has `instance` and `application` set so that the local copy is updated correctly\n                    const ensureProps = { instance: updatedDevice.instance || null, application: updatedDevice.application || null }\n                    this.updateLocalCopyOfDevice({ ...device, ...updatedDevice, ...ensureProps })\n                })\n            }\n        },\n\n        /**\n         * @param {Array<object>} devices - Array of devices to move\n         */\n        async moveDevicesToUnassigned (devices) {\n            const deviceIds = devices.map(device => device.id)\n            const data = await teamApi.bulkDeviceMove(this.team.id, deviceIds, 'unassigned')\n            if (data?.devices.length) {\n                Alerts.emit('Devices successfully unassigned.', 'confirmation')\n                data.devices.forEach(updatedDevice => {\n                    const device = this.devices.get(updatedDevice.id)\n                    // ensure the updated device has `instance` and `application` set so that the local copy is updated correctly\n                    const ensureProps = { instance: updatedDevice.instance || null, application: updatedDevice.application || null }\n                    this.updateLocalCopyOfDevice({ ...device, ...updatedDevice, ...ensureProps })\n                })\n            }\n        },\n\n        // Device loading\n        fullReloadOfData () {\n            this.checkedDevices = []\n            this.loadDevices(true)\n            this.pollForDeviceStatuses(true)\n        },\n\n        async loadDevices (reset) {\n            if (this.hasLoadedModel) {\n                await this.fetchDevices(reset)\n            }\n        },\n\n        async loadMoreDevices () {\n            await this.fetchDevices()\n        },\n\n        async pollForDeviceStatuses (reset) {\n            if (this.hasLoadedModel) {\n                await this.fetchAllDeviceStatuses(reset)\n            }\n        },\n\n        async fetchDevices (resetPage = false) {\n            if (resetPage) {\n                this.nextCursor = null\n            }\n\n            /// Params to send to the server\n            const nextCursor = this.nextCursor\n            const extraParams = {}\n\n            // Specific filtering\n            if (this.filter?.property && this.filter?.bucket) {\n                extraParams.filters = `${this.filter.property}:${this.filter.bucket}`\n            }\n\n            // Search and sort\n            if (this.searchTerm) {\n                extraParams.query = this.searchTerm\n            }\n            if (this.sort.key) {\n                extraParams.sort = this.sort.key\n                if (this.sort.direction) {\n                    extraParams.dir = this.sort.direction\n                }\n            }\n\n            // Actually fetch the data\n            const data = await this.fetchData(nextCursor, null, extraParams)\n\n            if (resetPage) {\n                this.devices = new Map()\n            }\n\n            data.devices.forEach(device => {\n                this.devices.set(device.id, device)\n            })\n\n            // Pagination\n            this.nextCursor = data.meta?.next_cursor || null\n\n            if (!extraParams.query) {\n                this.unsearchedHasMoreThanOnePage = this.moreThanOnePage\n            }\n\n            if (!extraParams.filters) {\n                this.unfilteredHasMoreThanOnePage = this.moreThanOnePage\n            }\n\n            this.loadingDevices = false\n        },\n\n        getOwnerSortKeyForDevice (device) {\n            if (!this.displayingTeam) {\n                return null\n            }\n\n            if (device.ownerType === 'application') {\n                return 'Application:' + device.application?.name || 'No Name'\n            }\n\n            if (device.ownerType === 'instance') {\n                return 'Instance:' + device.instance?.name || 'No Name'\n            }\n\n            return 'Unassigned'\n        }\n    }\n}\n</script>\n\n<style>\n.ff-dialog-content .ff-devices-ul {\n    list-style-type: disc;\n    list-style-position: inside;\n    columns: 2;\n}\n.ff-dialog-content .ff-devices-ul li {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n</style>\n","<template>\n    <ff-loading v-if=\"loading\" message=\"Loading Logs...\" />\n    <div v-if=\"showOfflineBanner\" class=\"ff-banner ff-banner-info my-2 rounded p-2 font-mono\">\n        <span>\n            <span>The Node-RED instance cannot be reached at this time. Please wait...</span>\n        </span>\n    </div>\n    <div v-if=\"!instance.meta || instance.meta.state === 'suspended'\" class=\"flex text-gray-500 justify-center italic mb-4 p-8\">\n        Logs unavailable\n    </div>\n    <div v-else :class=\"showOfflineBanner ? 'forge-log-offline-background' : ''\" class=\"mx-auto text-xs border bg-gray-800 text-gray-200 rounded p-2 font-mono\">\n        <div v-if=\"prevCursor\" class=\"flex\">\n            <a class=\"text-center w-full hover:text-blue-400 cursor-pointer pb-1\" @click=\"loadPrevious\">Load earlier...</a>\n        </div>\n        <div v-if=\"filteredLogEntries.length > 0\">\n            <span\n                v-for=\"(item, itemIdx) in filteredLogEntries\"\n                :key=\"itemIdx\"\n                class=\"whitespace-pre-wrap\"\n                :class=\"'forge-log-entry-level-' + item.level\"\n                data-el=\"instance-log-row\"\n            >\n                <template v-if=\"instance.ha?.replicas !== undefined\">\n                    [{{ item.src }}]\n                </template>\n                <span>{{ item.date }}</span>\n                <span>{{ \"  \" }}</span>\n                <span>{{ `[${item.level || ''}]`.padEnd(10, ' ') }}</span>\n                <span class=\"flex-grow break-all whitespace-pre-wrap inline-flex\">{{ item.msg }}</span>\n                <br v-if=\"itemIdx !== filteredLogEntries.length - 1\">\n            </span>\n        </div>\n    </div>\n</template>\n\n<script>\n\nimport InstanceApi from '../../../api/instances.js'\nimport Alerts from '../../../services/alerts.js'\nimport { createPollTimer } from '../../../utils/timers.js'\n\nconst POLL_TIME = 5000\n\nexport default {\n    name: 'LogsShared',\n    inheritAttrs: false,\n    props: {\n        instance: {\n            type: Object,\n            required: true\n        },\n        filter: {\n            default: null,\n            type: String,\n            required: false\n        }\n    },\n    emits: ['ha-instance-detected', 'new-range'],\n    data () {\n        return {\n            doneInitialLoad: false,\n            loading: true,\n            logEntries: [],\n            prevCursor: null,\n            nextCursor: null,\n            checkInterval: null,\n            showOfflineBanner: false,\n            /** @type {import('../../../utils/timers.js').PollTimer} */\n            pollTimer: null\n        }\n    },\n    computed: {\n        filteredLogEntries: function () {\n            if (this.filter && this.filter !== 'all') {\n                return this.logEntries.filter(l => l.src === this.filter)\n            } else {\n                return this.logEntries\n            }\n        }\n    },\n    watch: {\n        instance: 'fetchData'\n    },\n    async mounted () {\n        if (!this.instance.meta || this.instance.meta.state === 'suspended') {\n            this.loading = false\n        }\n        await this.fetchData()\n        // since the fetchdata is async, we need to check if the current page is\n        // still the log page before starting the poll timer\n        if (this.shouldPoll()) {\n            this.pollTimer = createPollTimer(this.pollTimerElapsed, POLL_TIME)\n        }\n    },\n    unmounted () {\n        this.stopPolling()\n    },\n    beforeUnmount () {\n        this.stopPolling()\n    },\n    methods: {\n        clear: function () {\n            this.logEntries = []\n        },\n        shouldPoll: function () {\n            return Object.hasOwnProperty.call(this.$route, 'meta') &&\n                Object.hasOwnProperty.call(this.$route.meta, 'shouldPoll') &&\n                this.$route.meta.shouldPoll\n        },\n        pollTimerElapsed: function () {\n            if (this.instance.meta && this.instance.meta.state !== 'suspended') {\n                this.loadNext()\n            }\n        },\n        stopPolling: function () {\n            if (this.pollTimer) {\n                this.pollTimer.stop()\n                this.pollTimer = null\n            }\n        },\n        fetchData: async function () {\n            if (this.instance.id) {\n                if (this.instance.meta && this.instance.meta.state !== 'suspended') {\n                    await this.loadItems(this.instance.id)\n                    this.loading = false\n                } else {\n                    this.logEntries = []\n                    this.prevCursor = null\n                }\n            }\n        },\n        loadPrevious: async function () {\n            this.loadItems(this.instance.id, this.prevCursor)\n        },\n        loadNext: async function () {\n            this.loadItems(this.instance.id, this.nextCursor)\n        },\n        loadItems: async function (instanceId, cursor) {\n            // don't poll if the page is not the log page\n            if (!this.shouldPoll()) {\n                this.stopPolling()\n                return\n            }\n\n            try {\n                const entries = await InstanceApi.getInstanceLogs(instanceId, cursor, null, { showAlert: false })\n                this.showOfflineBanner = false\n                if (!cursor) {\n                    this.prevCursor = null\n                    this.logEntries = []\n                }\n                const toPrepend = []\n                if (entries.log.length > 0) {\n                    entries.log.forEach(l => {\n                        const d = new Date(parseInt(l.ts.substring(0, l.ts.length - 4)))\n                        l.date = `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`\n                        if (typeof l.msg === 'undefined') {\n                            l.msg = 'undefined'\n                        } else if (typeof l.msg !== 'string') {\n                            l.msg = JSON.stringify(l.msg)\n                        }\n                        l.msg = l.msg.replace(/^[\\n]*/, '')\n                        if (!cursor || cursor[0] !== '-') {\n                            this.logEntries.push(l)\n                        } else {\n                            toPrepend.push(l)\n                        }\n                        if (l.src) {\n                            this.$emit('ha-instance-detected', l.src)\n                        }\n                    })\n                    if (toPrepend.length > 0) {\n                        this.logEntries = toPrepend.concat(this.logEntries)\n                    }\n                    if (!cursor || cursor[0] === '-') {\n                        this.prevCursor = entries.meta.previous_cursor\n                    }\n                    if (!cursor || cursor[0] !== '-') {\n                        this.nextCursor = entries.meta.next_cursor\n                    }\n                    if (entries.meta.first_entry && entries.meta.last_entry) {\n                        this.$emit('new-range', {\n                            first: entries.meta.first_entry,\n                            last: entries.meta.last_entry\n                        })\n                    }\n                }\n            } catch (error) {\n                // the page could have been switched while the async request was in progress, if so\n                // stop the polling and return immediately to avoid unnecessary error alerts\n                if (!this.shouldPoll()) {\n                    this.stopPolling()\n                    return\n                }\n                // log the error as warn for troubleshooting purposes\n                console.warn('Unable to retrieve Node-RED instance logs:', error)\n\n                // Error 503 is returned by the API when the launcher is offline.\n                // Error 500 is handled (and surfaced) by the `client.interceptors` in `../../../api/client.js`\n                // Error with data.code === 'project_suspended' is ignored - it is expected when the project is suspended\n                if (error.response?.status === 503) {\n                    if (this.showOfflineBanner === true) {\n                        return // only show the alert once\n                    }\n                    this.showOfflineBanner = true // show the \"offline\" banner\n                    Alerts.emit('The Node-RED instance cannot be reached at this time', 'warning', (POLL_TIME - 500))\n                } else if (error.response?.status !== 500 && error.response?.data?.code !== 'project_suspended') {\n                    // display an alert. Ensure it is visible for less time than\n                    // the polling interval to avoid multiple visible alerts\n                    const message = error.response?.data?.error || error.message\n                    Alerts.emit('Could not get Node-RED logs: ' + message, 'warning', (POLL_TIME - 500))\n                }\n            }\n        }\n    }\n}\n</script>\n\n<style scoped>\n.forge-log-offline-background {\n  background: repeating-linear-gradient(\n      -45deg,\n      #363848,\n      #363848 10px,\n      rgba(31, 41, 55, var(--tw-bg-opacity)) 10px,\n      rgba(31, 41, 55, var(--tw-bg-opacity)) 20px\n  );\n}\n</style>\n","<template>\n    <div ref=\"viewer\" data-el=\"ff-flow-previewer\" class=\"ff-flow-viewer\">\n        {{ flow }}\n    </div>\n</template>\n\n<script>\n\nimport FlowRenderer from '@flowfuse/flow-renderer'\n\nexport default {\n    name: 'FlowViewer',\n    props: {\n        flow: { required: true, type: Object }\n    },\n    mounted () {\n        this.render()\n    },\n    methods: {\n        render () {\n            const flowRenderer = new FlowRenderer()\n            flowRenderer.renderFlows(this.flow, {\n                container: this.$refs.viewer\n            })\n        }\n    }\n}\n</script>\n\n<style scoped>\n.ff-flow-viewer {\n    height: 600px;\n}\n</style>\n","@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n    /* .forge-block {\n        @apply bg-white;\n        @apply shadow;\n        @apply rounded;\n        @apply mx-2;\n        @apply sm:mx-4;\n        @apply py-6;\n        @apply px-2;\n        @apply sm:px-6;\n        @apply lg:px-8;\n        @apply mb-1;\n    } */\n\n    .forge-inner-block {\n        @apply py-6;\n    }\n\n    .forge-link {\n        @apply underline;\n        @apply hover:cursor-pointer;\n        @apply hover:text-blue-600;\n    }\n\n    .forge-button {\n        @apply px-3;\n        @apply py-1;\n        @apply bg-blue-900;\n        @apply border;\n        @apply border-blue-900;\n        @apply hover:bg-indigo-700;\n        @apply text-white;\n        @apply hover:text-gray-100;\n        @apply focus:text-gray-300;\n        @apply rounded-md;\n        @apply inline-flex;\n        @apply items-center;\n        @apply text-sm;\n        @apply focus:outline-none;\n        @apply focus:ring-2;\n        @apply focus:ring-offset-2;\n        @apply focus:ring-offset-gray-600;\n        @apply focus:ring-gray-400;\n\n        @apply disabled:opacity-30;\n        @apply disabled:cursor-not-allowed;\n    }\n\n    .forge-button-secondary {\n        @apply forge-button;\n        @apply bg-gray-100;\n        @apply border-gray-300;\n        @apply hover:bg-gray-300;\n        @apply hover:border-gray-300;\n        @apply text-gray-500;\n        @apply hover:text-gray-600;\n        @apply focus:text-gray-700;\n    }\n\n    .forge-button-tertiary {\n        @apply forge-button-secondary;\n        @apply bg-white;\n        @apply hover:bg-gray-100;\n    }\n\n    .forge-button-inline {\n        @apply forge-button-secondary;\n        @apply border-transparent;\n        @apply bg-white;\n        @apply hover:bg-gray-100;\n    }\n\n    .forge-button-inline-inactive {\n        @apply forge-button-inline;\n        pointer-events: none;\n    }\n\n    .forge-button-danger {\n        @apply forge-button;\n        @apply bg-white;\n        @apply border-red-700;\n        @apply hover:bg-red-700;\n        @apply text-red-700;\n        @apply hover:text-white;\n        @apply focus:text-white;\n        @apply focus:bg-red-700;\n\n        @apply disabled:border-gray-500;\n        @apply disabled:text-gray-600;\n        @apply disabled:bg-white;\n        @apply disabled:cursor-not-allowed;\n    }\n\n    .forge-button-small {\n        @apply px-2;\n        @apply py-1;\n        @apply text-xs;\n    }\n\n\n    .forge-button-set > :first-child {\n        border-top-right-radius: 0;\n        border-bottom-right-radius: 0;\n        border-right: none;\n    }\n    .forge-button-set > :last-child button {\n        border-top-left-radius: 0;\n        border-bottom-left-radius: 0;\n        border-left: none;\n    }\n\n    .forge-nav-item {\n        @apply flex items-center;\n        @apply pb-3;\n        @apply border-b-4 border-b-transparent;\n    }\n    .forge-nav-item:not(.forge-nav-item-active) {\n        @apply text-sm;\n    }\n    .forge-nav-item:not(.forge-nav-item-active):hover {\n        @apply text-blue-700;\n        @apply border-b-4;\n        @apply border-gray-400;\n    }\n\n    .forge-nav-item-active {\n        @apply text-sm;\n        @apply text-blue-700;\n        @apply border-b-4;\n        @apply border-blue-700;\n    }\n    .forge-badge {\n        @apply border;\n        @apply rounded-full;\n        @apply text-xs;\n        @apply px-2;\n        @apply py-1;\n        @apply inline-flex;\n        @apply items-center;\n    }\n    .forge-status-error,\n    .forge-status-crashed {\n        @apply bg-red-100;\n        @apply border-red-400;\n        @apply text-red-600;\n    }\n    .forge-status-suspended {\n        @apply bg-red-200;\n        @apply border-red-400;\n        @apply text-red-600;\n    }\n    .forge-status-stopped {\n        @apply bg-gray-100;\n        @apply border-gray-300;\n        @apply border-dashed;\n        @apply text-gray-700;\n    }\n    .forge-status-info {\n        @apply bg-gray-100;\n        @apply border-gray-300;\n        @apply border-dashed;\n        @apply text-gray-800;\n    }\n    .forge-status-starting {\n        @apply bg-green-100;\n        @apply border-green-300;\n        @apply border-dashed;\n        @apply text-green-700;\n    }\n    .forge-status-safe {\n        @apply bg-yellow-200;\n        @apply border-yellow-400;\n        @apply text-yellow-600;\n    }\n    .forge-status-warning {\n        @apply bg-yellow-400;\n        @apply border-yellow-700;\n        @apply text-yellow-900;\n    }\n    .forge-status-success,\n    .forge-status-connected,\n    .forge-status-running {\n        @apply bg-green-200;\n        @apply border-green-400;\n        @apply text-green-700;\n    }\n    .forge-status-importing {\n        @apply bg-green-100;\n        @apply border-green-300;\n        @apply border-dashed;\n        @apply text-green-700;\n    }\n    \n    .forge-badge-devmode {\n        @apply bg-purple-100;\n        @apply border-purple-600;\n        @apply text-purple-700;\n    }\n    .forge-badge-fleetmode {\n        @apply bg-teal-100;\n        @apply border-teal-600;\n        @apply text-teal-700;\n    } \n\n    th {\n        @apply text-left;\n        @apply font-medium;\n    }\n\n    input[type=\"text\"],\n    input[type=\"password\"],\n    input[type=\"radio\"],\n    input[type=\"checkbox\"],\n    select,\n    textarea,\n    .uneditable {\n        @apply text-sm;\n        @apply appearance-none;\n        @apply rounded;\n        @apply relative;\n        @apply font-normal;\n        @apply px-2;\n        @apply py-1;\n        @apply border;\n        @apply border-gray-300;\n        @apply placeholder-gray-500;\n        @apply text-gray-600;\n    }\n    input[type=\"radio\"],\n    input[type=\"checkbox\"] {\n        @apply mr-2;\n        @apply p-2;\n    }\n    .uneditable {\n        @apply border-opacity-0;\n    }\n\n    input[type=\"text\"]:focus,\n    input[type=\"password\"]:focus,\n    input[type=\"radio\"]:focus,\n    input[type=\"checkbox\"]:focus,\n    select:focus,\n    textarea:focus {\n        @apply outline-none;\n        @apply ring-0;\n        @apply border-indigo-500;\n    }\n    input[type=\"text\"]:disabled,\n    input[type=\"password\"]:disabled,\n    input[type=\"radio\"]:disabled,\n    input[type=\"checkbox\"]:disabled,\n    select:disabled,\n    textarea:disabled {\n        @apply cursor-not-allowed;\n        @apply opacity-60;\n    }\n\n    /* Make sure forge-log-entry-level-* all appear in tailwind.config.js safelist */\n    .forge-log-entry-level-system {\n        @apply text-blue-400;\n    }\n    .forge-log-entry-level-info {\n        @apply text-gray-100;\n    }\n    .forge-log-entry-level-warn {\n        @apply text-yellow-300;\n    }\n    .forge-log-entry-level-error {\n        @apply text-red-400;\n    }\n}\n\n@layer components {\n    .ff-layout--box--left,\n    .ff-layout--box--right {\n        @apply p-0 md:p-12;\n    }\n    .ff-layout--box--right .ff-layout--box--content {\n        @apply rounded-none md:rounded-xl m-auto;\n    }\n}","/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */\n\n/*\nDocument\n========\n*/\n\n/**\nUse a better box model (opinionated).\n*/\n\n*,\n::before,\n::after {\n\tbox-sizing: border-box;\n}\n\n/**\nUse a more readable tab size (opinionated).\n*/\n\nhtml {\n\t-moz-tab-size: 4;\n\ttab-size: 4;\n}\n\n/**\n1. Correct the line height in all browsers.\n2. Prevent adjustments of font size after orientation changes in iOS.\n*/\n\nhtml {\n\tline-height: 1.15; /* 1 */\n\t-webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/*\nSections\n========\n*/\n\n/**\nRemove the margin in all browsers.\n*/\n\nbody {\n\tmargin: 0;\n}\n\n/**\nImprove consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n*/\n\nbody {\n\tfont-family:\n\t\tsystem-ui,\n\t\t-apple-system, /* Firefox supports this but not yet `system-ui` */\n\t\t'Segoe UI',\n\t\tRoboto,\n\t\tHelvetica,\n\t\tArial,\n\t\tsans-serif,\n\t\t'Apple Color Emoji',\n\t\t'Segoe UI Emoji';\n}\n\n/*\nGrouping content\n================\n*/\n\n/**\n1. Add the correct height in Firefox.\n2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)\n*/\n\nhr {\n\theight: 0; /* 1 */\n\tcolor: inherit; /* 2 */\n}\n\n/*\nText-level semantics\n====================\n*/\n\n/**\nAdd the correct text decoration in Chrome, Edge, and Safari.\n*/\n\nabbr[title] {\n\ttext-decoration: underline dotted;\n}\n\n/**\nAdd the correct font weight in Edge and Safari.\n*/\n\nb,\nstrong {\n\tfont-weight: bolder;\n}\n\n/**\n1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)\n2. Correct the odd 'em' font sizing in all browsers.\n*/\n\ncode,\nkbd,\nsamp,\npre {\n\tfont-family:\n\t\tui-monospace,\n\t\tSFMono-Regular,\n\t\tConsolas,\n\t\t'Liberation Mono',\n\t\tMenlo,\n\t\tmonospace; /* 1 */\n\tfont-size: 1em; /* 2 */\n}\n\n/**\nAdd the correct font size in all browsers.\n*/\n\nsmall {\n\tfont-size: 80%;\n}\n\n/**\nPrevent 'sub' and 'sup' elements from affecting the line height in all browsers.\n*/\n\nsub,\nsup {\n\tfont-size: 75%;\n\tline-height: 0;\n\tposition: relative;\n\tvertical-align: baseline;\n}\n\nsub {\n\tbottom: -0.25em;\n}\n\nsup {\n\ttop: -0.5em;\n}\n\n/*\nTabular data\n============\n*/\n\n/**\n1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)\n2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)\n*/\n\ntable {\n\ttext-indent: 0; /* 1 */\n\tborder-color: inherit; /* 2 */\n}\n\n/*\nForms\n=====\n*/\n\n/**\n1. Change the font styles in all browsers.\n2. Remove the margin in Firefox and Safari.\n*/\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n\tfont-family: inherit; /* 1 */\n\tfont-size: 100%; /* 1 */\n\tline-height: 1.15; /* 1 */\n\tmargin: 0; /* 2 */\n}\n\n/**\nRemove the inheritance of text transform in Edge and Firefox.\n1. Remove the inheritance of text transform in Firefox.\n*/\n\nbutton,\nselect { /* 1 */\n\ttext-transform: none;\n}\n\n/**\nCorrect the inability to style clickable types in iOS and Safari.\n*/\n\nbutton,\n[type='button'],\n[type='reset'],\n[type='submit'] {\n\t-webkit-appearance: button;\n}\n\n/**\nRemove the inner border and padding in Firefox.\n*/\n\n::-moz-focus-inner {\n\tborder-style: none;\n\tpadding: 0;\n}\n\n/**\nRestore the focus styles unset by the previous rule.\n*/\n\n:-moz-focusring {\n\toutline: 1px dotted ButtonText;\n}\n\n/**\nRemove the additional ':invalid' styles in Firefox.\nSee: https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737\n*/\n\n:-moz-ui-invalid {\n\tbox-shadow: none;\n}\n\n/**\nRemove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.\n*/\n\nlegend {\n\tpadding: 0;\n}\n\n/**\nAdd the correct vertical alignment in Chrome and Firefox.\n*/\n\nprogress {\n\tvertical-align: baseline;\n}\n\n/**\nCorrect the cursor style of increment and decrement buttons in Safari.\n*/\n\n::-webkit-inner-spin-button,\n::-webkit-outer-spin-button {\n\theight: auto;\n}\n\n/**\n1. Correct the odd appearance in Chrome and Safari.\n2. Correct the outline style in Safari.\n*/\n\n[type='search'] {\n\t-webkit-appearance: textfield; /* 1 */\n\toutline-offset: -2px; /* 2 */\n}\n\n/**\nRemove the inner padding in Chrome and Safari on macOS.\n*/\n\n::-webkit-search-decoration {\n\t-webkit-appearance: none;\n}\n\n/**\n1. Correct the inability to style clickable types in iOS and Safari.\n2. Change font properties to 'inherit' in Safari.\n*/\n\n::-webkit-file-upload-button {\n\t-webkit-appearance: button; /* 1 */\n\tfont: inherit; /* 2 */\n}\n\n/*\nInteractive\n===========\n*/\n\n/*\nAdd the correct display in Chrome and Safari.\n*/\n\nsummary {\n\tdisplay: list-item;\n}\n","/**\n * Manually forked from SUIT CSS Base: https://github.com/suitcss/base\n * A thin layer on top of normalize.css that provides a starting point more\n * suitable for web applications.\n */\n\n/**\n * Removes the default spacing and border for appropriate elements.\n */\n\nblockquote,\ndl,\ndd,\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\nhr,\nfigure,\np,\npre {\n  margin: 0;\n}\n\nbutton {\n  background-color: transparent;\n  background-image: none;\n}\n\nfieldset {\n  margin: 0;\n  padding: 0;\n}\n\nol,\nul {\n  list-style: none;\n  margin: 0;\n  padding: 0;\n}\n\n/**\n * Tailwind custom reset styles\n */\n\n/**\n * 1. Use the user's configured `sans` font-family (with Tailwind's default\n *    sans-serif font stack as a fallback) as a sane default.\n * 2. Use Tailwind's default \"normal\" line-height so the user isn't forced\n *    to override it to ensure consistency even when using the default theme.\n */\n\nhtml {\n  font-family: theme('fontFamily.sans', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\"); /* 1 */\n  line-height: 1.5; /* 2 */\n}\n\n\n/**\n * Inherit font-family and line-height from `html` so users can set them as\n * a class directly on the `html` element.\n */\n\nbody {\n  font-family: inherit;\n  line-height: inherit;\n}\n\n/**\n * 1. Prevent padding and border from affecting element width.\n *\n *    We used to set this in the html element and inherit from\n *    the parent element for everything else. This caused issues\n *    in shadow-dom-enhanced elements like <details> where the content\n *    is wrapped by a div with box-sizing set to `content-box`.\n *\n *    https://github.com/mozdevs/cssremedy/issues/4\n *\n *\n * 2. Allow adding a border to an element by just adding a border-width.\n *\n *    By default, the way the browser specifies that an element should have no\n *    border is by setting it's border-style to `none` in the user-agent\n *    stylesheet.\n *\n *    In order to easily add borders to elements by just setting the `border-width`\n *    property, we change the default border-style for all elements to `solid`, and\n *    use border-width to hide them instead. This way our `border` utilities only\n *    need to set the `border-width` property instead of the entire `border`\n *    shorthand, making our border utilities much more straightforward to compose.\n *\n *    https://github.com/tailwindcss/tailwindcss/pull/116\n */\n\n*,\n::before,\n::after {\n  box-sizing: border-box; /* 1 */\n  border-width: 0; /* 2 */\n  border-style: solid; /* 2 */\n  border-color: currentColor; /* 2 */\n}\n\n/*\n * Ensure horizontal rules are visible by default\n */\n\nhr {\n  border-top-width: 1px;\n}\n\n/**\n * Undo the `border-style: none` reset that Normalize applies to images so that\n * our `border-{width}` utilities have the expected effect.\n *\n * The Normalize reset is unnecessary for us since we default the border-width\n * to 0 on all elements.\n *\n * https://github.com/tailwindcss/tailwindcss/issues/362\n */\n\nimg {\n  border-style: solid;\n}\n\ntextarea {\n  resize: vertical;\n}\n\ninput::placeholder,\ntextarea::placeholder {\n  opacity: 1;\n  color: theme('colors.gray.400', #a1a1aa);\n}\n\nbutton,\n[role=\"button\"] {\n  cursor: pointer;\n}\n\n/**\n * Override legacy focus reset from Normalize with modern Firefox focus styles.\n *\n * This is actually an improvement over the new defaults in Firefox in our testing,\n * as it triggers the better focus styles even for links, which still use a dotted\n * outline in Firefox by default.\n */\n \n:-moz-focusring {\n\toutline: auto;\n}\n\ntable {\n  border-collapse: collapse;\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n  font-size: inherit;\n  font-weight: inherit;\n}\n\n/**\n * Reset links to optimize for opt-in styling instead of\n * opt-out.\n */\n\na {\n  color: inherit;\n  text-decoration: inherit;\n}\n\n/**\n * Reset form element properties that are easy to forget to\n * style explicitly so you don't inadvertently introduce\n * styles that deviate from your design system. These styles\n * supplement a partial reset that is already applied by\n * normalize.css.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  padding: 0;\n  line-height: inherit;\n  color: inherit;\n}\n\n/**\n * Use the configured 'mono' font family for elements that\n * are expected to be rendered with a monospace font, falling\n * back to the system monospace stack if there is no configured\n * 'mono' font family.\n */\n\npre,\ncode,\nkbd,\nsamp {\n  font-family: theme('fontFamily.mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace);\n}\n\n/**\n * 1. Make replaced elements `display: block` by default as that's\n *    the behavior you want almost all of the time. Inspired by\n *    CSS Remedy, with `svg` added as well.\n *\n *    https://github.com/mozdevs/cssremedy/issues/14\n * \n * 2. Add `vertical-align: middle` to align replaced elements more\n *    sensibly by default when overriding `display` by adding a\n *    utility like `inline`.\n *\n *    This can trigger a poorly considered linting error in some\n *    tools but is included by design.\n * \n *    https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210\n */\n\nimg,\nsvg,\nvideo,\ncanvas,\naudio,\niframe,\nembed,\nobject {\n  display: block; /* 1 */\n  vertical-align: middle; /* 2 */\n}\n\n/**\n * Constrain images and videos to the parent width and preserve\n * their intrinsic aspect ratio.\n *\n * https://github.com/mozdevs/cssremedy/issues/14\n */\n\nimg,\nvideo {\n  max-width: 100%;\n  height: auto;\n}\n\n/**\n * Ensure the default browser behavior of the `hidden` attribute.\n */\n\n[hidden] {\n  display: none;\n}\n"],"names":[],"sourceRoot":""}