@@ -19,18 +19,68 @@ class LiebePanel extends HTMLElement {
1919 private _hass : HomeAssistant | null = null
2020 private root ?: ReactDOM . Root
2121 private initialized = false
22+ private instanceId = Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 )
23+ private visibilityHandler ?: ( ) => void
24+ private beforeUnloadHandler ?: ( ) => void
25+ private keepAliveInterval ?: number
26+ private lastInteraction = Date . now ( )
27+ private parentObserver ?: MutationObserver
28+ private reconnectCheckInterval ?: number
29+ private lastParentElement ?: Element | null
30+
31+ constructor ( ) {
32+ super ( )
33+ console . log ( `[Liebe Panel ${ this . instanceId } ] Constructor called` )
34+
35+ // Prevent panel from being garbage collected
36+ ; ( window as unknown as { __liebePanel ?: LiebePanel } ) . __liebePanel = this
37+
38+ // Override remove method to prevent removal
39+ const originalRemove = this . remove
40+ this . remove = ( ) => {
41+ console . log ( `[Liebe Panel ${ this . instanceId } ] remove() called - ignoring if we have hass` )
42+ if ( ! this . _hass ) {
43+ originalRemove . call ( this )
44+ }
45+ }
46+
47+ // Set up mutation observer to detect removal attempts
48+ this . setupParentObserver ( )
49+ }
2250
2351 set hass ( hass : HomeAssistant ) {
52+ console . log ( `[Liebe Panel ${ this . instanceId } ] hass setter called` , {
53+ hasHass : ! ! hass ,
54+ initialized : this . initialized ,
55+ connected : this . isConnected ,
56+ } )
2457 this . _hass = hass
2558 this . render ( )
2659 }
2760
2861 connectedCallback ( ) {
29- // Only initialize once - don't recreate on reconnection
30- if ( ! this . initialized ) {
62+ console . log ( `[Liebe Panel ${ this . instanceId } ] connectedCallback called` , {
63+ initialized : this . initialized ,
64+ hasHass : ! ! this . _hass ,
65+ isConnected : this . isConnected ,
66+ hasShadowRoot : ! ! this . shadowRoot ,
67+ } )
68+
69+ // Check if we need to re-initialize (e.g., if shadow root was lost)
70+ const needsInit = ! this . initialized || ! this . shadowRoot || ! this . root
71+
72+ if ( needsInit ) {
73+ console . log ( `[Liebe Panel ${ this . instanceId } ] Initializing/Re-initializing panel` )
3174 this . initialized = true
3275
33- const shadow = this . attachShadow ( { mode : 'open' } )
76+ // Create or get shadow root
77+ const shadow = this . shadowRoot || this . attachShadow ( { mode : 'open' } )
78+
79+ // Clear shadow root if re-initializing
80+ if ( this . shadowRoot && shadow . childNodes . length > 0 ) {
81+ shadow . innerHTML = ''
82+ }
83+
3484 const container = document . createElement ( 'div' )
3585 container . style . height = '100%'
3686 shadow . appendChild ( container )
@@ -52,28 +102,271 @@ class LiebePanel extends HTMLElement {
52102 }
53103
54104 this . root = ReactDOM . createRoot ( container )
105+
106+ // Track visibility changes
107+ this . visibilityHandler = ( ) => {
108+ console . log ( `[Liebe Panel ${ this . instanceId } ] Visibility changed` , {
109+ hidden : document . hidden ,
110+ visibilityState : document . visibilityState ,
111+ timestamp : new Date ( ) . toISOString ( ) ,
112+ isConnected : this . isConnected ,
113+ } )
114+
115+ // Update interaction time when page becomes visible
116+ if ( ! document . hidden ) {
117+ this . lastInteraction = Date . now ( )
118+ this . startKeepAlive ( )
119+
120+ // If we're not connected but should be, try to reconnect
121+ if ( ! this . isConnected && this . _hass && this . lastParentElement ) {
122+ console . log ( `[Liebe Panel ${ this . instanceId } ] Attempting to reconnect to parent` )
123+ try {
124+ this . lastParentElement . appendChild ( this )
125+ } catch ( error ) {
126+ console . error ( `[Liebe Panel ${ this . instanceId } ] Failed to reconnect:` , error )
127+ }
128+ }
129+ }
130+ }
131+ document . addEventListener ( 'visibilitychange' , this . visibilityHandler )
132+
133+ // Track page unload
134+ this . beforeUnloadHandler = ( ) => {
135+ console . log ( `[Liebe Panel ${ this . instanceId } ] Page unloading` )
136+ }
137+ window . addEventListener ( 'beforeunload' , this . beforeUnloadHandler )
138+
139+ // Start keep-alive mechanism
140+ this . startKeepAlive ( )
55141 }
56142
143+ // Store parent element reference
144+ if ( this . parentNode ) {
145+ this . lastParentElement = this . parentNode as Element
146+ }
147+
148+ // Set up parent observer if not already set
149+ if ( ! this . parentObserver && this . parentNode ) {
150+ this . setupParentObserver ( )
151+ }
152+
153+ // Start reconnect check
154+ this . startReconnectCheck ( )
155+
57156 this . render ( )
58157 }
59158
60159 disconnectedCallback ( ) {
61- // Do NOT unmount or cleanup - Home Assistant will re-add this element
160+ console . log ( `[Liebe Panel ${ this . instanceId } ] disconnectedCallback called` , {
161+ initialized : this . initialized ,
162+ hasHass : ! ! this . _hass ,
163+ timestamp : new Date ( ) . toISOString ( ) ,
164+ documentHidden : document . hidden ,
165+ visibilityState : document . visibilityState ,
166+ timeSinceLastInteraction : Date . now ( ) - this . lastInteraction ,
167+ parentElement : this . lastParentElement ?. tagName ,
168+ } )
169+
170+ // Do NOT stop keep-alive or cleanup - we want to stay ready for reconnection
171+ // Only clean up if we're truly being destroyed (no hass object)
172+ if ( ! this . _hass ) {
173+ this . stopKeepAlive ( )
174+ this . stopReconnectCheck ( )
175+ this . cleanupParentObserver ( )
176+ }
177+
178+ // Do NOT unmount or cleanup React - Home Assistant will re-add this element
62179 // when the user navigates back to the panel
63180 }
64181
182+ private startKeepAlive ( ) {
183+ // Clear any existing interval
184+ this . stopKeepAlive ( )
185+
186+ // Set up a keep-alive mechanism that periodically touches the DOM
187+ // to prevent Home Assistant from removing the panel
188+ this . keepAliveInterval = window . setInterval ( ( ) => {
189+ if ( this . isConnected ) {
190+ // Touch a DOM property to keep the element "active"
191+ void this . offsetHeight
192+
193+ // Periodically re-render if we have hass object
194+ if ( this . _hass && this . root ) {
195+ const timeSinceInteraction = Date . now ( ) - this . lastInteraction
196+ // Only re-render if page has been inactive for more than 5 minutes
197+ if ( timeSinceInteraction > 5 * 60 * 1000 ) {
198+ console . log ( `[Liebe Panel ${ this . instanceId } ] Keep-alive render triggered` )
199+ this . render ( )
200+ this . lastInteraction = Date . now ( )
201+ }
202+ }
203+ }
204+ } , 30000 ) // Check every 30 seconds
205+ }
206+
207+ private stopKeepAlive ( ) {
208+ if ( this . keepAliveInterval ) {
209+ clearInterval ( this . keepAliveInterval )
210+ this . keepAliveInterval = undefined
211+ }
212+ }
213+
214+ private setupParentObserver ( ) {
215+ // Watch for removal from parent
216+ this . parentObserver = new MutationObserver ( ( mutations ) => {
217+ mutations . forEach ( ( mutation ) => {
218+ if ( mutation . type === 'childList' && mutation . removedNodes . length > 0 ) {
219+ mutation . removedNodes . forEach ( ( node ) => {
220+ if (
221+ node === this ||
222+ ( node . nodeType === Node . ELEMENT_NODE && ( node as Element ) . contains ( this ) )
223+ ) {
224+ console . log (
225+ `[Liebe Panel ${ this . instanceId } ] Detected removal from parent, attempting to prevent`
226+ )
227+ // If we're being removed and we have a parent, try to add ourselves back
228+ if ( mutation . target && this . _hass ) {
229+ // Store the parent for reconnection attempts
230+ this . lastParentElement = mutation . target as Element
231+
232+ setTimeout ( ( ) => {
233+ if ( ! this . isConnected && mutation . target ) {
234+ console . log ( `[Liebe Panel ${ this . instanceId } ] Re-adding panel to parent` )
235+ try {
236+ mutation . target . appendChild ( this )
237+ } catch ( error ) {
238+ console . error (
239+ `[Liebe Panel ${ this . instanceId } ] Failed to re-add to parent:` ,
240+ error
241+ )
242+ }
243+ }
244+ } , 0 )
245+ }
246+ }
247+ } )
248+ }
249+ } )
250+ } )
251+
252+ // Observe multiple levels up the DOM tree
253+ if ( this . parentNode ) {
254+ this . parentObserver . observe ( this . parentNode , { childList : true , subtree : true } )
255+
256+ // Also observe the document body for more aggressive monitoring
257+ if ( document . body && ! document . body . contains ( this ) ) {
258+ this . parentObserver . observe ( document . body , { childList : true , subtree : true } )
259+ }
260+ }
261+ }
262+
263+ private cleanupParentObserver ( ) {
264+ if ( this . parentObserver ) {
265+ this . parentObserver . disconnect ( )
266+ this . parentObserver = undefined
267+ }
268+ }
269+
270+ private startReconnectCheck ( ) {
271+ // Clear any existing interval
272+ this . stopReconnectCheck ( )
273+
274+ // Check periodically if we need to reconnect
275+ this . reconnectCheckInterval = window . setInterval ( ( ) => {
276+ if ( ! this . isConnected && this . _hass && this . lastParentElement && ! document . hidden ) {
277+ console . log ( `[Liebe Panel ${ this . instanceId } ] Reconnect check: attempting to reconnect` )
278+
279+ // Try to find the panel container in Home Assistant
280+ const panelContainer =
281+ document . querySelector ( 'partial-panel-resolver' ) ||
282+ document . querySelector ( '[id^="panel-"]' ) ||
283+ this . lastParentElement
284+
285+ if ( panelContainer && ! panelContainer . contains ( this ) ) {
286+ try {
287+ panelContainer . appendChild ( this )
288+ console . log (
289+ `[Liebe Panel ${ this . instanceId } ] Successfully reconnected to panel container`
290+ )
291+ } catch ( error ) {
292+ console . error (
293+ `[Liebe Panel ${ this . instanceId } ] Failed to reconnect during check:` ,
294+ error
295+ )
296+ }
297+ }
298+ }
299+ } , 5000 ) // Check every 5 seconds
300+ }
301+
302+ private stopReconnectCheck ( ) {
303+ if ( this . reconnectCheckInterval ) {
304+ clearInterval ( this . reconnectCheckInterval )
305+ this . reconnectCheckInterval = undefined
306+ }
307+ }
308+
65309 private render ( ) {
310+ console . log ( `[Liebe Panel ${ this . instanceId } ] render called` , {
311+ hasRoot : ! ! this . root ,
312+ hasHass : ! ! this . _hass ,
313+ initialized : this . initialized ,
314+ } )
315+
66316 if ( ! this . root || ! this . _hass ) return
67- this . root . render (
68- React . createElement (
69- React . StrictMode ,
70- null ,
71- React . createElement ( Provider , { hass : this . _hass } , React . createElement ( PanelApp ) )
317+
318+ try {
319+ this . root . render (
320+ React . createElement (
321+ React . StrictMode ,
322+ null ,
323+ React . createElement ( Provider , { hass : this . _hass } , React . createElement ( PanelApp ) )
324+ )
72325 )
73- )
326+ } catch ( error ) {
327+ console . error ( `[Liebe Panel ${ this . instanceId } ] Render error:` , error )
328+ }
74329 }
75330}
76331
77332// Register custom element with environment-specific name
78333const panelConfig = getPanelConfig ( )
79334customElements . define ( panelConfig . elementName , LiebePanel )
335+
336+ // Global panel guardian - ensures panel stays connected
337+ let globalPanelCheck : number | undefined
338+ const startGlobalPanelGuardian = ( ) => {
339+ if ( globalPanelCheck ) return
340+
341+ globalPanelCheck = window . setInterval ( ( ) => {
342+ const panel = ( window as unknown as { __liebePanel ?: LiebePanel } ) . __liebePanel
343+ if ( panel && ! panel . isConnected && document . visibilityState === 'visible' ) {
344+ console . log (
345+ '[Global Panel Guardian] Panel disconnected while page visible, attempting recovery'
346+ )
347+
348+ // Try to find where the panel should be
349+ const possibleContainers = [
350+ document . querySelector ( 'partial-panel-resolver' ) ,
351+ document . querySelector ( '[id^="panel-"]' ) ,
352+ document . querySelector ( 'ha-panel-iframe' ) ,
353+ document . querySelector ( '.view' ) ,
354+ ] . filter ( Boolean )
355+
356+ for ( const container of possibleContainers ) {
357+ if ( container && ! container . contains ( panel ) ) {
358+ try {
359+ container . appendChild ( panel )
360+ console . log ( '[Global Panel Guardian] Successfully restored panel to container' )
361+ break
362+ } catch ( error ) {
363+ // Continue trying other containers
364+ }
365+ }
366+ }
367+ }
368+ } , 10000 ) // Check every 10 seconds
369+ }
370+
371+ // Start the guardian
372+ startGlobalPanelGuardian ( )
0 commit comments