2026-03-22 03:30:21 +00:00
< template >
2026-03-31 07:03:57 +01:00
< div class = "relative flex-1 min-h-0 bg-black/40 overflow-hidden app-session-frame-safe" >
2026-03-22 03:30:21 +00:00
< Transition name = "content-fade" >
< div v-if = "loading" class="absolute inset-0 z-10 flex items-center justify-center bg-black/40" >
< svg class = "animate-spin h-8 w-8 text-blue-400" viewBox = "0 0 24 24" fill = "none" >
< circle class = "opacity-25" cx = "12" cy = "12" r = "10" stroke = "currentColor" stroke -width =" 4 " />
< path class = "opacity-75" fill = "currentColor" d = "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</ svg >
</ div >
</ Transition >
2026-05-19 19:21:43 -04:00
< div
v-if = "appUrl && !iframeBlocked"
class = "absolute inset-0 app-session-frame-scroll-host"
tabindex = "-1"
@pointerdown ="focusIframe"
@focusin ="focusIframe"
>
2026-05-19 18:29:04 -04:00
< iframe
ref = "iframeRef"
:key = "refreshKey"
:src = "appUrl"
class = "w-full h-full border-0 iframe-scrollbar-hide"
title = "App content"
tabindex = "0"
2026-05-19 19:21:43 -04:00
@load ="handleIframeLoad"
2026-05-19 18:29:04 -04:00
@error ="$emit('iframeError')"
/>
</ div >
2026-03-22 03:30:21 +00:00
<!-- Iframe blocked fallback -->
< Transition name = "content-fade" >
< div v-if = "iframeBlocked" class="absolute inset-0 z-10 flex flex-col items-center justify-center" >
< div class = "text-center px-8" >
< div class = "w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/5 border border-white/10 flex items-center justify-center" >
< svg class = "w-8 h-8 text-white/40" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 1.5 " d = "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</ svg >
</ div >
2026-06-11 00:24:40 -04:00
< h3 class = "text-lg font-semibold text-white mb-2" >{{ blockedReason ? blockedTitle : ( mustOpenNewTab ? 'This app opens in a new tab' : 'App not reachable' ) }}</ h3 >
2026-03-22 03:30:21 +00:00
< p class = "text-white/50 text-sm mb-6" >
< template v-if = "mustOpenNewTab" >{{ appTitle }} sets security headers that prevent iframe embedding. < br > Open it in a new browser tab instead .</ template >
2026-06-11 00:24:40 -04:00
< template v-else-if = "blockedReason">{{ blockedReason }}<br><span v-if="autoRetryCount > 0" class="text-yellow-400/70" > Checking again automatically ( {{ autoRetryCount }} ) ... </ span ></ template >
2026-03-22 03:30:21 +00:00
< template v-else >{{ appTitle }} may still be starting up or the container is stopped. < br >< span v-if = "autoRetryCount > 0" class="text-yellow-400/70" > Retrying automatically ( {{ autoRetryCount }} ) ... </ span ></ template >
</ p >
2026-05-06 09:23:57 -04:00
< div class = "flex flex-wrap items-center justify-center gap-3" >
2026-03-22 03:30:21 +00:00
< button
v-if = "!mustOpenNewTab"
@click ="$emit('refresh')"
class = "glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
>
< svg class = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</ svg >
Retry now
</ button >
< button
@click ="$emit('openNewTabAndBack')"
class = "glass-button px-6 py-3 rounded-lg text-sm font-semibold inline-flex items-center gap-2"
>
< svg class = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path stroke -linecap =" round " stroke -linejoin =" round " stroke -width =" 2 " d = "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</ svg >
Open in new tab
</ button >
</ div >
</ div >
</ div >
</ Transition >
< div v-if = "!appUrl" class="absolute inset-0 flex items-center justify-center" >
< div class = "text-center px-8" >
< h3 class = "text-lg font-semibold text-white mb-2" > App not configured </ h3 >
< p class = "text-white/50 text-sm" > No URL found for {{ appId }}</ p >
</ div >
</ div >
</ div >
</ template >
< script setup lang = "ts" >
2026-05-19 18:29:04 -04:00
import { nextTick , ref , watch } from 'vue'
2026-03-22 03:30:21 +00:00
2026-05-19 18:29:04 -04:00
const props = defineProps < {
2026-03-22 03:30:21 +00:00
appUrl : string
appId : string
appTitle : string
loading : boolean
iframeBlocked : boolean
mustOpenNewTab : boolean
autoRetryCount : number
refreshKey : number
2026-06-11 00:24:40 -04:00
blockedReason ?: string
blockedTitle ?: string
2026-03-22 03:30:21 +00:00
} > ()
2026-05-19 19:21:43 -04:00
const emit = defineEmits < {
2026-03-22 03:30:21 +00:00
iframeLoad : []
iframeError : []
refresh : []
openNewTabAndBack : []
} > ()
const iframeRef = ref < HTMLIFrameElement | null >( null )
2026-05-19 19:21:43 -04:00
function focusIframe () {
iframeRef . value ? . focus ({ preventScroll : true })
}
async function handleIframeLoad () {
emit ( 'iframeLoad' )
await nextTick ()
requestAnimationFrame ( focusIframe )
}
2026-05-19 18:29:04 -04:00
watch (() => [ props . appUrl , props . refreshKey , props . iframeBlocked ], async () => {
if ( ! props . appUrl || props . iframeBlocked ) return
await nextTick ()
2026-05-19 19:21:43 -04:00
requestAnimationFrame ( focusIframe )
2026-05-19 18:29:04 -04:00
}, { immediate : true })
2026-03-22 03:30:21 +00:00
defineExpose ({ iframeRef })
</ script >