feat: add container security hardening and Fedimint setup wizard

Add --cap-drop=ALL, --security-opt=no-new-privileges:true to all
non-privileged containers. Per-app capability grants for apps needing
CHOWN/SETUID/SETGID. Read-only root filesystem with tmpfs for
compatible apps (searxng, grafana, uptime-kuma, filebrowser,
photoprism, vaultwarden). Add Fedimint "Create a Community" goal
with 4-step wizard. Fix deploy script cp -rf for audio directory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-05 08:24:56 +00:00
co-authored by Claude Opus 4.6
parent da3bf44cdb
commit 0bc7251e22
14 changed files with 186 additions and 50 deletions
@@ -6,6 +6,7 @@ const currentName = ref('')
const playing = ref(false)
const currentTime = ref(0)
const duration = ref(0)
const error = ref<string | null>(null)
function play(src: string, name: string) {
if (!audio.value) {
@@ -15,6 +16,7 @@ function play(src: string, name: string) {
})
audio.value.addEventListener('loadedmetadata', () => {
duration.value = audio.value?.duration ?? 0
error.value = null
})
audio.value.addEventListener('ended', () => {
playing.value = false
@@ -24,8 +26,14 @@ function play(src: string, name: string) {
})
audio.value.addEventListener('play', () => {
playing.value = true
error.value = null
})
audio.value.addEventListener('error', () => {
playing.value = false
error.value = 'Could not play audio. File Browser may not be running.'
})
}
error.value = null
if (currentSrc.value === src && playing.value) {
audio.value.pause()
@@ -78,5 +86,6 @@ export function useAudioPlayer() {
duration,
progress,
currentSrc,
error,
}
}
+17 -13
View File
@@ -25,24 +25,28 @@ export function useMobileBackButton() {
function updateTabBarHeight() {
if (typeof window === 'undefined') return
// Try to find the mobile tab bar element
const tabBar = document.querySelector('[data-mobile-tab-bar]') as HTMLElement
if (tabBar) {
if (tabBar && tabBar.offsetHeight > 0) {
tabBarHeight.value = tabBar.offsetHeight
} else {
// Fallback: read from CSS variable if available
const cssVar = getComputedStyle(document.documentElement)
.getPropertyValue('--mobile-tab-bar-height')
.trim()
if (cssVar) {
const height = parseFloat(cssVar)
if (!isNaN(height)) {
tabBarHeight.value = height
}
return
}
// Fallback: read from CSS variable if available
const cssVar = getComputedStyle(document.documentElement)
.getPropertyValue('--mobile-tab-bar-height')
.trim()
if (cssVar) {
const height = parseFloat(cssVar)
if (!isNaN(height) && height > 0) {
tabBarHeight.value = height
return
}
}
// Final fallback: keep current value (don't reset to 0)
}
onMounted(() => {
+42
View File
@@ -189,6 +189,48 @@ export const GOALS: GoalDefinition[] = [
estimatedTime: '~40 min + sync time',
difficulty: 'intermediate',
},
{
id: 'setup-fedimint',
title: 'Create a Community',
subtitle: 'Start a Fedimint federation for private, scalable Bitcoin',
icon: 'community',
category: 'community',
requiredApps: ['bitcoin-knots', 'fedimint'],
steps: [
{
id: 'install-bitcoin',
title: 'Install Bitcoin Node',
description: 'Bitcoin Knots provides the base layer that Fedimint connects to for on-chain transactions and consensus.',
appId: 'bitcoin-knots',
action: 'install',
isAutomatic: true,
},
{
id: 'install-fedimint',
title: 'Install Fedimint',
description: 'Fedimint is a federated Bitcoin mint. Guardians collectively manage funds using threshold signatures — no single point of failure.',
appId: 'fedimint',
action: 'install',
isAutomatic: true,
},
{
id: 'configure-guardian',
title: 'Set Up Guardian UI',
description: 'Open the Guardian UI (port 8175) to configure your federation name, set the guardian threshold, and initialize the mint.',
action: 'configure',
isAutomatic: false,
},
{
id: 'share-invite',
title: 'Share Invite Code',
description: 'Generate and share your federation invite code with community members so they can join and start using ecash.',
action: 'info',
isAutomatic: false,
},
],
estimatedTime: '~30 min + sync time',
difficulty: 'intermediate',
},
{
id: 'create-identity',
title: 'Create My Identity',
+21 -2
View File
@@ -180,6 +180,13 @@
background: transparent;
}
/* On mobile, leave room for close button + tab bar below AIUI */
@media (max-width: 767px) {
.chat-iframe-mobile {
padding-bottom: calc(var(--mobile-tab-bar-height, 72px) + 52px);
}
}
/* Chat placeholder (no AIUI URL) */
.chat-placeholder {
flex: 1;
@@ -1175,6 +1182,16 @@ html:has(body.video-background-active)::before {
background: rgba(0, 0, 0, 0.4);
}
/* ── Mobile floating back/close button (always 8px above tab bar) ──── */
.mobile-back-btn {
position: fixed;
left: 1rem;
right: 1rem;
bottom: calc(var(--mobile-tab-bar-height, 72px) + 8px);
z-index: 40;
filter: drop-shadow(0 10px 25px rgba(0, 0, 0, 0.5));
}
/* ── Cloud Audio Player (mini bar) ──── */
.cloud-audio-player {
@@ -1194,8 +1211,9 @@ html:has(body.video-background-active)::before {
display: flex;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
width: 2.75rem;
height: 2.75rem;
min-width: 2.75rem;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: none;
@@ -1203,6 +1221,7 @@ html:has(body.video-background-active)::before {
cursor: pointer;
flex-shrink: 0;
transition: all 0.15s ease;
padding: 0;
}
.cloud-audio-player-btn:hover {
background: rgba(255, 255, 255, 0.2);
+1 -1
View File
@@ -5,7 +5,7 @@ export interface GoalDefinition {
title: string
subtitle: string
icon: string
category: 'commerce' | 'payments' | 'storage' | 'identity' | 'network' | 'backup'
category: 'commerce' | 'payments' | 'storage' | 'identity' | 'network' | 'backup' | 'community'
requiredApps: string[]
steps: GoalStep[]
estimatedTime: string
+2 -9
View File
@@ -9,13 +9,9 @@
</button>
<!-- Mobile Full-Width Back Button -->
<button
<button
@click="goBack"
class="md:hidden fixed left-4 right-4 z-40 glass-button px-6 py-3 rounded-lg font-medium shadow-2xl flex items-center justify-center gap-2"
:style="{
bottom: bottomPosition,
filter: 'drop-shadow(0 10px 25px rgba(0, 0, 0, 0.5))'
}"
class="md:hidden mobile-back-btn glass-button px-6 py-3 rounded-lg font-medium shadow-2xl flex items-center justify-center gap-2"
>
<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="M15 19l-7-7 7-7" />
@@ -432,12 +428,9 @@ import { useRouter, useRoute } from 'vue-router'
import { useAppStore } from '../stores/app'
import { useAppLauncherStore } from '../stores/appLauncher'
import { PackageState } from '../types/api'
import { useMobileBackButton } from '../composables/useMobileBackButton'
import { useModalKeyboard } from '@/composables/useModalKeyboard'
import { dummyApps } from '../utils/dummyApps'
const { bottomPosition } = useMobileBackButton()
const router = useRouter()
const route = useRoute()
const store = useAppStore()
+17 -17
View File
@@ -1,6 +1,6 @@
<template>
<div class="chat-fullscreen">
<!-- Close button + connection indicator (desktop only) -->
<!-- Close button + connection indicator (desktop: top-right pill) -->
<div class="chat-mode-pill hidden md:flex">
<button class="chat-close-btn" @click="closeChat">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -15,18 +15,6 @@
/>
</div>
<!-- Mobile close button (bottom, thumb-reachable) -->
<button
class="md:hidden fixed left-4 right-4 z-40 glass-button px-6 py-3 rounded-lg font-medium shadow-2xl flex items-center justify-center gap-2"
:style="{ bottom: bottomPosition, filter: 'drop-shadow(0 10px 25px rgba(0, 0, 0, 0.5))' }"
@click="closeChat"
>
<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="M6 18L18 6M6 6l12 12" />
</svg>
Close Chat
</button>
<!-- Loading indicator while iframe loads -->
<Transition name="fade">
<div v-if="aiuiUrl && !aiuiConnected" class="chat-loading">
@@ -37,12 +25,12 @@
</div>
</Transition>
<!-- AIUI iframe -->
<!-- AIUI iframe on mobile, leave room for close bar + tab bar at bottom -->
<iframe
v-if="aiuiUrl"
ref="aiuiFrame"
:src="aiuiUrl"
class="chat-iframe"
class="chat-iframe chat-iframe-mobile"
sandbox="allow-scripts allow-same-origin allow-forms"
allow="microphone"
style="background: transparent"
@@ -65,6 +53,20 @@
</p>
</div>
</div>
<!-- Mobile close bar: sits above the tab bar, below the AIUI content -->
<div class="md:hidden mobile-back-btn flex items-center justify-center">
<button
class="w-full glass-button px-6 py-2.5 rounded-lg font-medium flex items-center justify-center gap-2 text-sm"
style="background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);"
@click="closeChat"
>
<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="M6 18L18 6M6 6l12 12" />
</svg>
Close Chat
</button>
</div>
</div>
</template>
@@ -72,9 +74,7 @@
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
import { ContextBroker } from '@/services/contextBroker'
import { useMobileBackButton } from '@/composables/useMobileBackButton'
const { bottomPosition } = useMobileBackButton()
const router = useRouter()
const aiuiFrame = ref<HTMLIFrameElement | null>(null)
const aiuiConnected = ref(false)
-2
View File
@@ -160,12 +160,10 @@ import { ref, computed, watch } from 'vue'
import { useRouter, useRoute, RouterLink } from 'vue-router'
import { useAppStore } from '../stores/app'
import { useCloudStore } from '../stores/cloud'
import { useMobileBackButton } from '../composables/useMobileBackButton'
import CloudToolbar from '../components/cloud/CloudToolbar.vue'
import FileGrid from '../components/cloud/FileGrid.vue'
import { useAudioPlayer } from '../composables/useAudioPlayer'
useMobileBackButton()
const router = useRouter()
const route = useRoute()
const store = useAppStore()
+1 -2
View File
@@ -286,12 +286,11 @@
<!-- Mobile Bottom Tab Bar - glass piece 5 -->
<nav
v-show="!chatFullscreen"
ref="mobileTabBar"
data-mobile-tab-bar
class="md:hidden fixed bottom-0 left-0 right-0 border-t border-glass-border shadow-glass z-50 glass-piece"
:class="{ 'glass-throw-tabbar': showZoomIn }"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px);"
style="background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(18px); -webkit-backdrop-filter: blur(18px); padding-bottom: env(safe-area-inset-bottom, 0px);"
>
<div class="flex justify-around items-center px-2 py-3 relative">
<RouterLink