Keep embedded app URL stable as initial runtime state arrives
This commit is contained in:
@@ -591,3 +591,16 @@ the approved consent presentation. Dashboard typecheck passes. Logs:
|
|||||||
`/tmp/archy-signer-queue-related-tests.log`,
|
`/tmp/archy-signer-queue-related-tests.log`,
|
||||||
`/tmp/archy-signer-queue-typecheck.log`. Production UI build/deployment still
|
`/tmp/archy-signer-queue-typecheck.log`. Production UI build/deployment still
|
||||||
pending; physical companion causality remains unverified.
|
pending; physical companion causality remains unverified.
|
||||||
|
|
||||||
|
The signer UI candidate `715e86c9` was deployed to dev only, with UI backup;
|
||||||
|
backend/session secret/app containers were unchanged. Live served-browser checks
|
||||||
|
with isolated signing RPC fixtures passed at 390/1440px, but an earlier run saw
|
||||||
|
two first-request responses after iframe startup. Do not erase that failed run.
|
||||||
|
Inspection found fallback `http://host:7778` versus runtime `http://host:7778/`
|
||||||
|
changes the raw iframe src during initial state discovery. Canonicalizing the
|
||||||
|
computed URL prevents this semantically identical destination from reloading.
|
||||||
|
The new regression observes no reactive iframe-source change for this update,
|
||||||
|
while real path changes still propagate and cancel prior pending consent.
|
||||||
|
29 focused routing/session/signer tests pass; final rebuild/redeployment remains
|
||||||
|
pending. The first browser attempt was also missing the signed-in local marker
|
||||||
|
and redirected to login; this fixture error was corrected separately.
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
|||||||
import MobileGamepad from './appSession/MobileGamepad.vue'
|
import MobileGamepad from './appSession/MobileGamepad.vue'
|
||||||
import {
|
import {
|
||||||
type DisplayMode, DISPLAY_MODE_KEY, NEW_TAB_APPS, IFRAME_BLOCKED_APPS,
|
type DisplayMode, DISPLAY_MODE_KEY, NEW_TAB_APPS, IFRAME_BLOCKED_APPS,
|
||||||
initialDisplayMode, resolveAppUrl, resolveAppTitle,
|
initialDisplayMode, resolveAppUrl, resolveAppTitle, canonicalAppUrl,
|
||||||
} from './appSession/appSessionConfig'
|
} from './appSession/appSessionConfig'
|
||||||
import { launchBlockedReason, resolveAppIcon } from './apps/appsConfig'
|
import { launchBlockedReason, resolveAppIcon } from './apps/appsConfig'
|
||||||
import { PackageState } from '@/types/api'
|
import { PackageState } from '@/types/api'
|
||||||
@@ -267,7 +267,7 @@ const screensaverSuppressedApps = new Set([
|
|||||||
const appUrl = computed(() => {
|
const appUrl = computed(() => {
|
||||||
const runtimeUrl = store.data?.['package-data']?.[appId.value]?.installed?.['interface-addresses']?.main?.['lan-address'] || undefined
|
const runtimeUrl = store.data?.['package-data']?.[appId.value]?.installed?.['interface-addresses']?.main?.['lan-address'] || undefined
|
||||||
const deepPath = props.pathProp ?? (route.query.path as string | undefined)
|
const deepPath = props.pathProp ?? (route.query.path as string | undefined)
|
||||||
return resolveAppUrl(appId.value, deepPath, runtimeUrl)
|
return canonicalAppUrl(resolveAppUrl(appId.value, deepPath, runtimeUrl))
|
||||||
})
|
})
|
||||||
|
|
||||||
function closeRouteSession() {
|
function closeRouteSession() {
|
||||||
@@ -291,6 +291,9 @@ const nostrBridge = useNostrBridge(identity.getStoredIdentity, {
|
|||||||
frameWindow: () => iframeRef.value?.contentWindow ?? null,
|
frameWindow: () => iframeRef.value?.contentWindow ?? null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// An actual destination change invalidates consent queued for the previous app page.
|
||||||
|
watch(appUrl, () => nostrBridge.cancelPending())
|
||||||
|
|
||||||
// --- Display mode ---
|
// --- Display mode ---
|
||||||
|
|
||||||
function setMode(mode: DisplayMode) {
|
function setMode(mode: DisplayMode) {
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ vi.mock('../appSession/useNostrBridge', () => ({
|
|||||||
consentError: { value: '' },
|
consentError: { value: '' },
|
||||||
approveConsent: vi.fn(),
|
approveConsent: vi.fn(),
|
||||||
denyConsent: vi.fn(),
|
denyConsent: vi.fn(),
|
||||||
|
cancelPending: vi.fn(),
|
||||||
|
dispose: vi.fn(),
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { computed, ref, watch, nextTick } from 'vue'
|
||||||
|
import { canonicalAppUrl } from '../appSessionConfig'
|
||||||
|
|
||||||
|
describe('stable app iframe URL', () => {
|
||||||
|
it('does not replace the iframe source when runtime discovery adds only a root slash', async () => {
|
||||||
|
const reported = ref('http://node.test:7778')
|
||||||
|
const frameUrl = computed(() => canonicalAppUrl(reported.value))
|
||||||
|
let changes = 0
|
||||||
|
const stop = watch(frameUrl, () => { changes++ })
|
||||||
|
expect(frameUrl.value).toBe('http://node.test:7778/')
|
||||||
|
reported.value = 'http://node.test:7778/'
|
||||||
|
await nextTick()
|
||||||
|
expect(changes).toBe(0)
|
||||||
|
reported.value = 'http://node.test:7778/project/demo'
|
||||||
|
await nextTick()
|
||||||
|
expect(changes).toBe(1)
|
||||||
|
stop()
|
||||||
|
})
|
||||||
|
it('preserves deep paths, query strings and fragments', () => {
|
||||||
|
expect(canonicalAppUrl('https://node.test:7778/project/demo?view=preview#player'))
|
||||||
|
.toBe('https://node.test:7778/project/demo?view=preview#player')
|
||||||
|
})
|
||||||
|
it('keeps an absent app URL absent instead of opening the dashboard inside itself', () => {
|
||||||
|
expect(canonicalAppUrl('')).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -119,6 +119,12 @@ export const HOST_FRAME_APPS = new Set([
|
|||||||
/** Sites known to block iframes -- skip the timeout and go straight to fallback */
|
/** Sites known to block iframes -- skip the timeout and go straight to fallback */
|
||||||
export const IFRAME_BLOCKED_APPS = new Set<string>([])
|
export const IFRAME_BLOCKED_APPS = new Set<string>([])
|
||||||
|
|
||||||
|
/** Stable iframe src while the initial runtime state replaces the fallback URL. */
|
||||||
|
export function canonicalAppUrl(url: string): string {
|
||||||
|
if (!url) return ''
|
||||||
|
try { return new URL(url, window.location.origin).href } catch { return url }
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolve app URL using direct port mapping (source of truth) */
|
/** Resolve app URL using direct port mapping (source of truth) */
|
||||||
export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?: string): string {
|
export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?: string): string {
|
||||||
// Demo: route to the app's mock UI or real external site (mempool.space,
|
// Demo: route to the app's mock UI or real external site (mempool.space,
|
||||||
|
|||||||
Reference in New Issue
Block a user