hot fixes to utc-6

This commit is contained in:
Dorian
2026-03-12 12:56:59 +00:00
parent f07ce10b1a
commit 73e0a1b74d
26 changed files with 1123 additions and 76 deletions

View File

@@ -274,7 +274,7 @@ class RPCClient {
})
}
async getNostrPubkey(): Promise<{ nostr_pubkey: string }> {
async getNostrPubkey(): Promise<{ nostr_pubkey: string; nostr_npub?: string }> {
return this.call({
method: 'node.nostr-pubkey',
params: {},

View File

@@ -269,7 +269,7 @@ function isIdentityAwareApp(url: string): boolean {
async function sendIdentityIfSupported() {
if (!store.url || !isIdentityAwareApp(store.url)) return
try {
const res = await rpcClient.call<{ identities: Array<{ id: string; name: string; did: string; pubkey: string; is_default: boolean; nostr_pubkey?: string }> }>({ method: 'identity.list' })
const res = await rpcClient.call<{ identities: Array<{ id: string; name: string; did: string; pubkey: string; is_default: boolean; nostr_pubkey?: string; nostr_npub?: string }> }>({ method: 'identity.list' })
const defaultId = res.identities?.find(i => i.is_default) || res.identities?.[0]
if (!defaultId) return
// Sign a timestamp challenge to prove ownership
@@ -286,6 +286,7 @@ async function sendIdentityIfSupported() {
name: defaultId.name,
pubkey: defaultId.pubkey,
nostr_pubkey: defaultId.nostr_pubkey || null,
nostr_npub: defaultId.nostr_npub || null,
challenge,
signature: sigRes.signature
}, '*')

View File

@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { rpcClient } from '@/api/rpc-client'
/** Apps that set X-Frame-Options or CSP frame-ancestors, blocking iframe embedding.
* Verified by checking response headers from each app container.
@@ -10,11 +11,12 @@ import { ref } from 'vue'
*/
const IFRAME_BLOCKED_HOSTS: string[] = []
/** External sites proxied through nginx to strip X-Frame-Options for iframe embedding */
const EXTERNAL_PROXY: Record<string, string> = {
'botfights.net': '/ext/botfights/',
'484.kitchen': '/ext/484-kitchen/',
'present.l484.com': '/ext/arch-presentation/',
/** External sites proxied through nginx on dedicated ports (strips X-Frame-Options).
* Each site gets its own port so SPAs work at root — no subpath rewriting needed. */
const EXTERNAL_PROXY_PORT: Record<string, number> = {
'botfights.net': 8901,
'484.kitchen': 8902,
'present.l484.com': 8903,
}
function mustOpenInNewTab(url: string): boolean {
@@ -82,10 +84,10 @@ function toEmbeddableUrl(url: string): string {
const u = new URL(url)
const origin = window.location.origin
// External sites proxied through nginx to strip X-Frame-Options
const extProxy = EXTERNAL_PROXY[u.hostname]
if (extProxy) {
return `${origin}${extProxy}`
// External sites proxied through nginx on dedicated ports
const extPort = EXTERNAL_PROXY_PORT[u.hostname]
if (extPort) {
return `${window.location.protocol}//${window.location.hostname}:${extPort}/`
}
const proxyPath = PORT_TO_PROXY[u.port]
@@ -132,6 +134,42 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
}
}
// NIP-07 postMessage handler — responds to nostr-request from iframe apps
async function handleNostrRequest(event: MessageEvent) {
if (!event.data || event.data.type !== 'nostr-request') return
const { id, method, params } = event.data
const source = event.source as Window | null
if (!source) return
try {
let result: unknown
if (method === 'getPublicKey') {
const res = await rpcClient.call<{ nostr_pubkey: string }>({ method: 'node.nostr-pubkey' })
result = res.nostr_pubkey
} else if (method === 'signEvent') {
const res = await rpcClient.call<unknown>({ method: 'identity.nostr-sign', params: { event: params.event } })
result = res
} else if (method === 'getRelays') {
result = {}
} else {
throw new Error(`Unsupported NIP-07 method: ${method}`)
}
source.postMessage({ type: 'nostr-response', id, result }, '*')
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'
source.postMessage({ type: 'nostr-response', id, error: message }, '*')
}
}
// Listen for NIP-07 requests only while an app is open
watch(isOpen, (open) => {
if (open) {
window.addEventListener('message', handleNostrRequest)
} else {
window.removeEventListener('message', handleNostrRequest)
}
})
return {
isOpen,
url,

View File

@@ -215,13 +215,13 @@ input[type="radio"]:active + * {
.chat-mode-pill {
position: absolute;
top: calc(env(safe-area-inset-top, 0px) + 1.25rem);
top: calc(env(safe-area-inset-top, 0px) + 2.25rem);
right: calc(env(safe-area-inset-right, 0px) + 1.25rem);
z-index: 10;
}
@media (min-width: 768px) {
.chat-mode-pill {
top: 1.25rem;
top: 2.25rem;
right: 1.25rem;
}
}

View File

@@ -515,7 +515,7 @@ export const dummyApps: Record<string, PackageDataEntry> = {
'static-files': {
license: 'MIT',
instructions: 'Decentralized media streaming platform',
icon: 'https://indeehub.studio/favicon.ico'
icon: '/assets/img/app-icons/indeehub.ico'
},
manifest: {
id: 'indeedhub',
@@ -545,6 +545,244 @@ export const dummyApps: Record<string, PackageDataEntry> = {
},
status: ServiceStatus.Running
}
},
'botfights': {
state: PackageState.Running,
'static-files': {
license: 'MIT',
instructions: 'AI bot arena',
icon: '/assets/img/app-icons/botfights.svg'
},
manifest: {
id: 'botfights',
title: 'BotFights',
version: '1.0.0',
description: {
short: 'AI bot arena — build, train, and battle autonomous agents',
long: 'BotFights is an AI bot arena where you can build, train, and battle autonomous agents. Create intelligent bots using various strategies, pit them against other players\' creations, and climb the leaderboard. Features real-time battle visualization, multiple game modes, and a growing community of bot builders.'
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': 'https://botfights.net',
website: 'https://botfights.net',
'donation-url': null
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': {
main: { 'tor-address': '', 'lan-address': 'https://botfights.net' }
},
status: ServiceStatus.Running
}
},
'nwnn': {
state: PackageState.Running,
'static-files': {
license: 'MIT',
instructions: 'Decentralized news aggregator',
icon: '/assets/img/app-icons/nwnn.png'
},
manifest: {
id: 'nwnn',
title: 'Next Web News Network',
version: '1.0.0',
description: {
short: 'Decentralized news aggregator, synced from Telegram',
long: 'Next Web News Network (NWNN) is a decentralized news aggregation platform that curates and syncs content from Telegram channels. Stay informed with the latest developments in Bitcoin, decentralization, and sovereign technology. Clean reading experience with no ads or tracking.'
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': 'https://nwnn.l484.com',
website: 'https://nwnn.l484.com',
'donation-url': null
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': {
main: { 'tor-address': '', 'lan-address': 'https://nwnn.l484.com' }
},
status: ServiceStatus.Running
}
},
'484-kitchen': {
state: PackageState.Running,
'static-files': {
license: 'MIT',
instructions: 'K484 application platform',
icon: '/assets/img/app-icons/484-kitchen.png'
},
manifest: {
id: '484-kitchen',
title: '484 Kitchen',
version: '1.0.0',
description: {
short: 'K484 application platform',
long: '484 Kitchen is a creative application platform from the K484 collective. Explore experimental tools, interactive experiences, and cutting-edge web applications built with a focus on sovereignty and decentralization.'
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': 'https://484.kitchen',
website: 'https://484.kitchen',
'donation-url': null
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': {
main: { 'tor-address': '', 'lan-address': 'https://484.kitchen' }
},
status: ServiceStatus.Running
}
},
'call-the-operator': {
state: PackageState.Running,
'static-files': {
license: 'MIT',
instructions: 'Escape the Matrix',
icon: '/assets/img/app-icons/call-the-operator.png'
},
manifest: {
id: 'call-the-operator',
title: 'Call the Operator',
version: '1.0.0',
description: {
short: 'Escape the Matrix — explore decentralized alternatives',
long: 'Call the Operator is an interactive guide to escaping the centralized matrix. Discover decentralized alternatives to mainstream services, learn about self-sovereignty, and take back control of your digital life. Beautiful dreamcore aesthetic with immersive 3D visuals.'
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': 'https://cta.tx1138.com',
website: 'https://cta.tx1138.com',
'donation-url': null
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': {
main: { 'tor-address': '', 'lan-address': 'https://cta.tx1138.com' }
},
status: ServiceStatus.Running
}
},
'arch-presentation': {
state: PackageState.Running,
'static-files': {
license: 'MIT',
instructions: 'Archipelago presentation',
icon: '/assets/img/app-icons/arch-presentation.png'
},
manifest: {
id: 'arch-presentation',
title: 'Arch Presentation',
version: '1.0.0',
description: {
short: 'Archipelago: The Future of Decentralized Infrastructure',
long: 'The official Archipelago presentation deck. Learn about the vision, architecture, and roadmap of the Archipelago Bitcoin Node OS. Interactive slides showcasing the future of decentralized personal infrastructure, self-sovereign computing, and the Web5 stack.'
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': 'https://present.l484.com',
website: 'https://present.l484.com',
'donation-url': null
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': {
main: { 'tor-address': '', 'lan-address': 'https://present.l484.com' }
},
status: ServiceStatus.Running
}
},
'syntropy-institute': {
state: PackageState.Running,
'static-files': {
license: 'MIT',
instructions: 'Frequency analysis and therapy',
icon: '/assets/img/app-icons/syntropy-institute.png'
},
manifest: {
id: 'syntropy-institute',
title: 'Syntropy Institute',
version: '1.0.0',
description: {
short: 'Medicine Reimagined — frequency analysis and therapy',
long: 'Syntropy Institute presents a new paradigm in health and wellness through frequency analysis and therapy. Explore cutting-edge research into bioresonance, quantum biology, and the energetic foundations of health. A bridge between ancient healing wisdom and modern technology.'
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': 'https://syntropy.institute',
website: 'https://syntropy.institute',
'donation-url': null
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': {
main: { 'tor-address': '', 'lan-address': 'https://syntropy.institute' }
},
status: ServiceStatus.Running
}
},
't-zero': {
state: PackageState.Running,
'static-files': {
license: 'MIT',
instructions: 'Documentary series',
icon: '/assets/img/app-icons/t-zero.png'
},
manifest: {
id: 't-zero',
title: 'T-0',
version: '1.0.0',
description: {
short: 'Documentary series on decentralization and Bitcoin',
long: 'T-0 (Tee Minus Zero) is a documentary series exploring the intersection of decentralization, Bitcoin, and personal sovereignty. Follow the stories of builders, dreamers, and freedom advocates creating the infrastructure for a more sovereign future.'
},
'release-notes': 'Initial release',
license: 'MIT',
'wrapper-repo': '',
'upstream-repo': '',
'support-site': '',
'marketing-site': 'https://teeminuszero.net',
website: 'https://teeminuszero.net',
'donation-url': null
},
installed: {
'current-dependents': {},
'current-dependencies': {},
'last-backup': null,
'interface-addresses': {
main: { 'tor-address': '', 'lan-address': 'https://teeminuszero.net' }
},
status: ServiceStatus.Running
}
}
}

View File

@@ -72,6 +72,7 @@
</svg>
{{ t('common.launch') }}
</button>
<template v-if="!isWebOnly">
<button
v-if="pkg.state === 'stopped'"
@click="startApp"
@@ -111,6 +112,7 @@
</svg>
{{ t('common.uninstall') }}
</button>
</template>
</div>
</div>
@@ -144,6 +146,7 @@
<!-- Uninstall Icon Button -->
<button
v-if="!isWebOnly"
@click="uninstallApp"
class="flex-shrink-0 w-10 h-10 rounded-lg bg-red-600/20 border border-red-600/40 text-red-300 hover:bg-red-600/30 transition-colors flex items-center justify-center"
:title="t('common.uninstall')"
@@ -159,6 +162,7 @@
<button
v-if="canLaunch"
@click="launchApp"
:class="isWebOnly ? 'col-span-2' : ''"
class="glass-button glass-button-sm px-4 py-2.5 rounded-lg text-sm font-semibold flex items-center justify-center gap-2"
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -166,6 +170,7 @@
</svg>
{{ t('common.launch') }}
</button>
<template v-if="!isWebOnly">
<button
v-if="pkg.state === 'stopped'"
@click="startApp"
@@ -197,6 +202,7 @@
</svg>
{{ t('common.restart') }}
</button>
</template>
</div>
</div>
</div>
@@ -330,8 +336,8 @@
</div>
</div>
<!-- Requirements Card -->
<div class="glass-card p-6">
<!-- Requirements Card (hidden for web-only apps) -->
<div v-if="!isWebOnly" class="glass-card p-6">
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.requirements') }}</h3>
<div class="space-y-3">
<div class="flex items-start gap-3">
@@ -478,6 +484,20 @@ const { t } = useI18n()
const appId = computed(() => route.params.id as string)
// Web-only app detection (no container — external websites)
const WEB_ONLY_APP_URLS: Record<string, string> = {
'indeedhub': 'https://archipelago.indeehub.studio',
'botfights': 'https://botfights.net',
'nwnn': 'https://nwnn.l484.com',
'484-kitchen': 'https://484.kitchen',
'call-the-operator': 'https://cta.tx1138.com',
'arch-presentation': 'https://present.l484.com',
'syntropy-institute': 'https://syntropy.institute',
't-zero': 'https://teeminuszero.net',
}
const isWebOnly = computed(() => appId.value in WEB_ONLY_APP_URLS)
/** Map route/marketplace app IDs to backend package keys (container names). */
const ROUTE_TO_PACKAGE_KEY: Record<string, string> = {
mempool: 'mempool-web',
@@ -625,7 +645,8 @@ const backButtonText = computed(() => {
// Check if app has a UI interface and is running
const canLaunch = computed(() => {
if (!pkg.value) return false
// For dummy apps, allow launch if running (they have interface addresses)
// Web-only apps are always launchable
if (isWebOnly.value) return true
// For real apps, check for UI interface
const hasUI = pkg.value.manifest.interfaces?.main?.ui || pkg.value.installed?.['interface-addresses']?.main
const isRunning = pkg.value.state === 'running'
@@ -693,12 +714,18 @@ function goBack() {
function launchApp() {
if (!pkg.value) return
const isDev = import.meta.env.DEV
const id = appId.value
// Web-only apps — use their external URL directly
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
if (webOnlyUrl) {
useAppLauncherStore().open({ url: webOnlyUrl, title: pkg.value.manifest.title })
return
}
// Special handling for apps with Docker containers
// TODO: Replace dummy app URLs with real URLs when apps are packaged
const appUrls: Record<string, { dev: string, prod: string }> = {
'lorabell': {
dev: 'http://192.168.1.166',

View File

@@ -273,7 +273,7 @@ const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
'indeedhub': {
state: 'running' as PackageState,
manifest: { id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: { short: 'Bitcoin documentary streaming platform', long: '' }, 'release-notes': '', license: '', 'wrapper-repo': '', 'upstream-repo': '', 'support-site': '', 'marketing-site': '', 'donation-url': null },
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/indeedhub.png' },
'static-files': { license: '', instructions: '', icon: '/assets/img/app-icons/indeehub.ico' },
},
'botfights': {
state: 'running' as PackageState,

View File

@@ -1,7 +1,7 @@
<template>
<div class="chat-fullscreen">
<!-- Close button + connection indicator (desktop: top-right pill) -->
<div class="chat-mode-pill hidden md:flex">
<div class="chat-mode-pill flex">
<button class="chat-close-btn" :aria-label="t('chat.closeAssistant')" @click="closeChat">
<svg class="w-4 h-4" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
@@ -15,19 +15,12 @@
/>
</div>
<!-- Mobile back button -->
<button class="chat-mobile-back md:hidden" :aria-label="t('common.goBack')" @click="closeChat">
<svg class="w-5 h-5" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<!-- Loading indicator while checking availability or iframe loads -->
<!-- Loading indicator while checking availability -->
<Transition name="fade">
<div v-if="aiuiAvailable === null || (aiuiUrl && !aiuiConnected)" class="chat-loading" role="status" aria-live="polite">
<div v-if="aiuiAvailable === null" class="chat-loading" role="status" aria-live="polite">
<div class="glass-card p-8 flex flex-col items-center gap-4">
<div class="chat-loading-spinner" aria-hidden="true" />
<p class="text-sm text-white/60">{{ aiuiAvailable === null ? t('chat.loadingAssistant') : t('chat.loadingAssistant') }}</p>
<p class="text-sm text-white/60">{{ t('chat.loadingAssistant') }}</p>
</div>
</div>
</Transition>
@@ -39,13 +32,13 @@
:src="aiuiUrl"
:title="t('chat.aiAssistant')"
class="chat-iframe chat-iframe-mobile"
sandbox="allow-scripts allow-same-origin allow-forms"
allow="microphone"
style="background: transparent"
/>
<!-- Fallback when no AIUI URL configured -->
<div v-else class="chat-placeholder">
<!-- Fallback when AIUI is not deployed -->
<div v-else-if="aiuiAvailable === false" class="chat-placeholder">
<div class="chat-placeholder-inner">
<div class="chat-placeholder-icon">
<svg class="w-8 h-8 text-white/40" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -84,7 +77,7 @@ const aiuiUrl = computed(() => {
const envUrl = import.meta.env.VITE_AIUI_URL
if (envUrl) return `${envUrl}?embedded=true`
// In production, only return the URL if we've confirmed AIUI files exist
if (import.meta.env.PROD && aiuiAvailable.value === true) return '/aiui/?embedded=true'
if (import.meta.env.PROD && aiuiAvailable.value === true) return `/aiui/?embedded=true&v=${Date.now()}`
return ''
})
@@ -177,20 +170,4 @@ onBeforeUnmount(() => {
opacity: 0;
}
.chat-mobile-back {
position: absolute;
top: 0.75rem;
left: 0.75rem;
z-index: 20;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(8px);
color: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(255, 255, 255, 0.15);
}
</style>