feat: external iframes, container startup UX, release notes modal
- Add https: to CSP frame-src so external site iframes (BotFights, 484 Kitchen, etc.) load without being blocked by Content-Security-Policy - Show spinner + "Starting..." on marketplace cards for containers that are booting up, preventing users from re-installing running apps - Add spinner to transitional state badges (starting/stopping/installing) on installed app cards in Apps view - Add "What's New" button to Settings version card with release notes modal covering recent highlights in layman-friendly language Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e4089287a3
commit
bccef62585
@ -17,7 +17,7 @@ server {
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-DNS-Prefetch-Control "off" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:*; frame-src 'self' http://$host:*; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# AIUI SPA (Chat mode iframe)
|
||||
# Use =404 fallback instead of index.html to prevent serving HTML with wrong
|
||||
@ -721,7 +721,7 @@ server {
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-DNS-Prefetch-Control "off" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:*; frame-src 'self' http://$host:*; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ws: wss: http://$host:* https:; frame-src 'self' http://$host:* https:; frame-ancestors 'self'; base-uri 'self'; form-action 'self';" always;
|
||||
|
||||
# AIUI SPA (Chat mode iframe)
|
||||
location /aiui/ {
|
||||
|
||||
@ -118,9 +118,19 @@
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex items-center px-2 py-1 rounded text-xs font-medium"
|
||||
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
||||
:class="getStatusClass(pkg.state)"
|
||||
>
|
||||
<svg
|
||||
v-if="pkg.state === 'starting' || pkg.state === 'installing' || pkg.state === 'stopping' || pkg.state === 'restarting'"
|
||||
class="animate-spin h-3 w-3"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<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"></path>
|
||||
</svg>
|
||||
{{ pkg.state }}
|
||||
</span>
|
||||
<span class="text-xs text-white/50">
|
||||
|
||||
@ -177,37 +177,49 @@
|
||||
</p>
|
||||
|
||||
<div class="flex gap-2 mt-auto">
|
||||
<span
|
||||
v-if="isInstalled(app.id)"
|
||||
<!-- Installed & starting up (transitional state) -->
|
||||
<span
|
||||
v-if="isInstalled(app.id) && isStartingUp(app.id)"
|
||||
class="flex-1 px-4 py-2 bg-yellow-500/15 border border-yellow-500/30 rounded-lg text-yellow-200 text-sm font-medium text-center cursor-default flex items-center justify-center gap-2"
|
||||
>
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<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"></path>
|
||||
</svg>
|
||||
{{ getInstalledState(app.id) === 'installing' ? 'Installing...' : 'Starting...' }}
|
||||
</span>
|
||||
<!-- Installed & ready -->
|
||||
<span
|
||||
v-else-if="isInstalled(app.id)"
|
||||
class="flex-1 px-4 py-2 bg-white/20 rounded-lg text-white/60 text-sm font-medium text-center cursor-default"
|
||||
>
|
||||
{{ t('marketplace.alreadyInstalled') }}
|
||||
</span>
|
||||
>
|
||||
{{ t('marketplace.alreadyInstalled') }}
|
||||
</span>
|
||||
<button
|
||||
v-if="isInstalled(app.id)"
|
||||
v-if="isInstalled(app.id) && !isStartingUp(app.id)"
|
||||
@click.stop="launchInstalledApp(app)"
|
||||
class="px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium"
|
||||
>
|
||||
{{ t('common.launch') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="app.source === 'local' || app.dockerImage"
|
||||
v-else-if="!isInstalled(app.id) && (app.source === 'local' || app.dockerImage)"
|
||||
data-controller-install-btn
|
||||
@click.stop="app.source === 'local' ? installApp(app) : installCommunityApp(app)"
|
||||
:disabled="installingApps.has(app.id)"
|
||||
:disabled="installingApps.has(app.id)"
|
||||
class="flex-1 px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span v-if="installingApps.has(app.id)" class="flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<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"></path>
|
||||
</svg>
|
||||
{{ installingApps.get(app.id)?.message || t('common.installing') }}
|
||||
</span>
|
||||
<span v-else>{{ t('common.install') }}</span>
|
||||
>
|
||||
<span v-if="installingApps.has(app.id)" class="flex items-center justify-center gap-2">
|
||||
<svg class="animate-spin h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<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"></path>
|
||||
</svg>
|
||||
{{ installingApps.get(app.id)?.message || t('common.installing') }}
|
||||
</span>
|
||||
<span v-else>{{ t('common.install') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
v-else-if="!isInstalled(app.id)"
|
||||
disabled
|
||||
class="flex-1 px-4 py-2 bg-white/10 rounded-lg text-white/40 text-sm font-medium cursor-not-allowed"
|
||||
>
|
||||
@ -640,6 +652,26 @@ function isInstalled(appId: string): boolean {
|
||||
return aliases ? aliases.some((a) => a in installedPackages.value) : false
|
||||
}
|
||||
|
||||
/** Get the package state for an installed app (checks aliases too). */
|
||||
function getInstalledState(appId: string): string | null {
|
||||
const pkg = installedPackages.value[appId]
|
||||
if (pkg) return pkg.state
|
||||
const aliases = INSTALLED_ALIASES[appId]
|
||||
if (aliases) {
|
||||
for (const a of aliases) {
|
||||
const aliasPkg = installedPackages.value[a]
|
||||
if (aliasPkg) return aliasPkg.state
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** True if installed and currently in a transitional state (not yet ready). */
|
||||
function isStartingUp(appId: string): boolean {
|
||||
const state = getInstalledState(appId)
|
||||
return state !== null && state !== 'running' && state !== 'stopped' && state !== 'exited'
|
||||
}
|
||||
|
||||
function launchInstalledApp(app: MarketplaceApp) {
|
||||
appLauncher.openSession(app.id)
|
||||
}
|
||||
|
||||
@ -54,9 +54,59 @@
|
||||
</svg>
|
||||
<p class="text-xs font-semibold text-white/60 uppercase tracking-wide">{{ t('common.version') }}</p>
|
||||
</div>
|
||||
<p class="text-lg font-semibold text-white/95">{{ version }}</p>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-lg font-semibold text-white/95">{{ version }}</p>
|
||||
<button
|
||||
@click="showReleaseNotes = true"
|
||||
class="glass-button px-3 py-1.5 text-xs"
|
||||
>What's New</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Release Notes Modal -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="showReleaseNotes" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click="showReleaseNotes = false">
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div @click.stop class="glass-card p-6 max-w-lg w-full relative z-10 max-h-[80vh] overflow-y-auto">
|
||||
<div class="flex items-start justify-between gap-4 mb-5">
|
||||
<h3 class="text-xl font-semibold text-white">What's New in {{ version }}</h3>
|
||||
<button @click="showReleaseNotes = false" class="p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors" aria-label="Close">
|
||||
<svg class="w-5 h-5" 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" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4 text-sm text-white/80">
|
||||
<div>
|
||||
<h4 class="text-white font-medium mb-1">Mesh Radio Connection</h4>
|
||||
<p>Your LoRa mesh radio now connects reliably every time. If the USB port changes, the system automatically finds the device. A new "Connect" button lets you pick which radio to use.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-white font-medium mb-1">Rock-Solid Containers</h4>
|
||||
<p>Fixed crash loops that were causing apps to restart thousands of times. All containers now start cleanly and stay stable across reboots.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-white font-medium mb-1">Smarter App Store</h4>
|
||||
<p>Apps that are still starting up now show their progress instead of appearing as available to install again. No more accidentally re-installing running apps.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-white font-medium mb-1">Security Hardening</h4>
|
||||
<p>Backend services now run with strict sandboxing: read-only filesystem, restricted system calls, and least-privilege access. Your node is locked down by default.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-white font-medium mb-1">Tor by Default</h4>
|
||||
<p>Bitcoin and Lightning now route through Tor automatically for maximum privacy. Your node's network traffic is protected out of the box.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-white font-medium mb-1">Off-Grid Bitcoin</h4>
|
||||
<p>Receive Bitcoin block headers over mesh radio. A dead man's switch can broadcast your location to trusted contacts if you go silent. GPS sharing is opt-in only.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="showReleaseNotes = false" class="glass-button w-full mt-6 py-2 text-sm">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
|
||||
<!-- Session Card -->
|
||||
<div class="bg-black/20 rounded-xl px-5 py-4 border border-white/10 md:col-span-2">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
@ -965,6 +1015,7 @@ async function saveServerName() {
|
||||
}
|
||||
|
||||
const version = computed(() => store.serverInfo?.version || '0.0.0')
|
||||
const showReleaseNotes = ref(false)
|
||||
const serverTorAddressFromStore = computed(() => store.serverInfo?.['tor-address'] || null)
|
||||
const torAddressFromRpc = ref<string | null>(null)
|
||||
const serverTorAddress = computed(() => serverTorAddressFromStore.value || torAddressFromRpc.value)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user