fix: container DNS, nginx chown, onboarding guard, seed UX, install flow

Backend:
- Add --add-host host.containers.internal:host-gateway to LND and Bitcoin
  Knots containers (fixes DNS resolution failure in rootless podman)
- Add --user 0:0 and DAC_OVERRIDE to nginx UI sidecar containers
  (fixes chown crash in rootless podman for bitcoin-ui, electrs-ui, lnd-ui)
- Add hostadd to Rust Podman API client for web UI container installs
- Add Chromium privacy flags to kiosk launcher (disable telemetry)

Frontend:
- Fix onboarding reset on raw IP visits (trust localStorage as first-class
  signal, skip boot screen when server is up but not onboarded)
- Fix seed regression: persist challenge indices in sessionStorage so going
  back from Verify doesn't change which words are asked
- Remove glass container from seed Generate/Verify/Restore screens
- Add Back button to Restore from Seed screen
- Replace Network card: Tor (purple), VPN status (orange), Bitcoin sync (orange)
- Add ElectrumX to curated app list with correct .webp icon
- Install flow: navigate to My Apps immediately with toast, hide
  installed/installing apps from marketplace and discover views

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-01 13:06:57 +01:00
parent b4a57e83d0
commit 6d3704fff5
14 changed files with 3874 additions and 233 deletions

View File

@ -310,6 +310,7 @@ impl PodmanClient {
"portmappings": port_mappings, "portmappings": port_mappings,
"mounts": mounts, "mounts": mounts,
"env": env_map, "env": env_map,
"hostadd": ["host.containers.internal:host-gateway"],
"devices": manifest.app.devices.iter().map(|d| { "devices": manifest.app.devices.iter().map(|d| {
serde_json::json!({"path": d}) serde_json::json!({"path": d})
}).collect::<Vec<_>>(), }).collect::<Vec<_>>(),

File diff suppressed because it is too large Load Diff

View File

@ -47,6 +47,11 @@ while true; do
--disable-background-networking \ --disable-background-networking \
--disable-background-timer-throttling \ --disable-background-timer-throttling \
--disable-backgrounding-occluded-windows \ --disable-backgrounding-occluded-windows \
--disable-breakpad \
--disable-metrics \
--disable-metrics-reporting \
--metrics-recording-only \
--disable-domain-reliability \
--js-flags="--max-old-space-size=128" \ --js-flags="--max-old-space-size=128" \
--user-data-dir=/home/archipelago/.config/chromium-kiosk --user-data-dir=/home/archipelago/.config/chromium-kiosk
sleep 3 sleep 3

View File

@ -19,9 +19,11 @@ async function callWithRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T
} }
export async function isOnboardingComplete(): Promise<boolean> { export async function isOnboardingComplete(): Promise<boolean> {
// localStorage is set on completion and survives backend restarts/resets
if (localStorage.getItem('neode_onboarding_complete') === '1') return true
const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 2) const result = await callWithRetry(() => rpcClient.isOnboardingComplete(), 2)
if (result !== null) return result if (result !== null) return result
return localStorage.getItem('neode_onboarding_complete') === '1' return false
} }
export async function completeOnboarding(): Promise<void> { export async function completeOnboarding(): Promise<void> {

View File

@ -144,6 +144,7 @@ import { useServerStore } from '@/stores/server'
import { rpcClient } from '@/api/rpc-client' import { rpcClient } from '@/api/rpc-client'
import { useMarketplaceApp } from '@/composables/useMarketplaceApp' import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
import { useAppLauncherStore } from '@/stores/appLauncher' import { useAppLauncherStore } from '@/stores/appLauncher'
import { useToast } from '@/composables/useToast'
import DiscoverHero from './discover/DiscoverHero.vue' import DiscoverHero from './discover/DiscoverHero.vue'
import FeaturedApps from './discover/FeaturedApps.vue' import FeaturedApps from './discover/FeaturedApps.vue'
import AppGrid from './discover/AppGrid.vue' import AppGrid from './discover/AppGrid.vue'
@ -427,9 +428,13 @@ function startInstallPolling(appId: string, statusMessage: string) {
}, 1000) }, 1000)
} }
const toast = useToast()
async function installApp(app: MarketplaceApp) { async function installApp(app: MarketplaceApp) {
if (installingApps.has(app.id) || isInstalled(app.id)) return if (installingApps.has(app.id) || isInstalled(app.id)) return
installingApps.set(app.id, { id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0 }) installingApps.set(app.id, { id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0 })
toast.info(`Installing ${app.title ?? app.id} — check My Apps`)
router.push('/dashboard/apps').catch(() => {})
try { try {
const installUrl = app.url || app.manifestUrl || app.s9pkUrl const installUrl = app.url || app.manifestUrl || app.s9pkUrl
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' }) installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' })
@ -446,6 +451,8 @@ async function installApp(app: MarketplaceApp) {
async function installCommunityApp(app: MarketplaceApp) { async function installCommunityApp(app: MarketplaceApp) {
if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
installingApps.set(app.id, { id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0 }) installingApps.set(app.id, { id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0 })
toast.info(`Installing ${app.title ?? app.id} — check My Apps`)
router.push('/dashboard/apps').catch(() => {})
try { try {
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' }) installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' })
await rpcClient.call({ method: 'package.install', params: { id: app.id, dockerImage: app.dockerImage, version: app.version }, timeout: 180000 }) await rpcClient.call({ method: 'package.install', params: { id: app.id, dockerImage: app.dockerImage, version: app.version }, timeout: 180000 })

View File

@ -140,16 +140,16 @@
</div> </div>
<div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0"> <div class="home-card-stats space-y-3 mb-4 flex-1 min-h-0">
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg"> <div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="servicesDotColor"></div><span class="text-sm text-white/80">{{ t('home.servicesStatus') }}</span></div> <div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="torConnected ? 'bg-purple-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">Tor</span></div>
<span class="text-sm font-medium" :class="servicesStatusColor">{{ servicesStatusText }}</span> <span class="text-sm font-medium" :class="torConnected ? 'text-purple-400' : 'text-white/40'">{{ torConnected ? 'Connected' : 'Offline' }}</span>
</div> </div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg"> <div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="connectivityDotColor"></div><span class="text-sm text-white/80">{{ t('home.connectivity') }}</span></div> <div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="vpnConnected ? 'bg-orange-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">VPN</span></div>
<span class="text-sm font-medium" :class="connectivityColor">{{ connectivityText }}</span> <span class="text-sm font-medium" :class="vpnConnected ? 'text-orange-400' : 'text-white/40'">{{ vpnConnected ? 'Connected' : 'Not configured' }}</span>
</div> </div>
<div class="flex items-center justify-between p-3 bg-white/5 rounded-lg"> <div class="flex items-center justify-between p-3 bg-white/5 rounded-lg">
<div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full bg-blue-400"></div><span class="text-sm text-white/80">{{ t('home.runningApps') }}</span></div> <div class="flex items-center gap-3"><div class="w-2 h-2 rounded-full" :class="systemStats.bitcoinAvailable ? 'bg-orange-400' : 'bg-white/40'"></div><span class="text-sm text-white/80">Bitcoin</span></div>
<span class="text-sm text-white/80 font-medium">{{ runningCount }}</span> <span class="text-sm font-medium" :class="systemStats.bitcoinAvailable ? 'text-orange-400' : 'text-white/40'">{{ bitcoinSyncDisplay }}</span>
</div> </div>
</div> </div>
<div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0"> <div class="home-card-buttons flex gap-2 mt-auto pt-4 shrink-0">
@ -306,13 +306,20 @@ const quickLaunchApps = [
{ id: '484-kitchen', name: '484 Kitchen', icon: '/assets/img/app-icons/484-kitchen.png', bg: '', padded: false }, { id: '484-kitchen', name: '484 Kitchen', icon: '/assets/img/app-icons/484-kitchen.png', bg: '', padded: false },
] ]
const servicesAllRunning = computed(() => appCount.value > 0 && runningCount.value === appCount.value) // Network card data
const servicesStatusText = computed(() => appCount.value === 0 ? t('home.noApps') : servicesAllRunning.value ? t('home.allRunning') : `${runningCount.value}/${appCount.value} ${t('home.runningLabel')}`) const torConnected = computed(() => {
const servicesStatusColor = computed(() => appCount.value === 0 ? 'text-white/60' : servicesAllRunning.value ? 'text-green-400' : 'text-yellow-400') const torAddr = store.data?.['server-info']?.['tor-address']
const servicesDotColor = computed(() => appCount.value === 0 ? 'bg-white/40' : servicesAllRunning.value ? 'bg-green-400' : 'bg-yellow-400') return !!torAddr && torAddr.length > 0
const connectivityText = computed(() => store.isConnected ? t('common.connected') : t('common.disconnected')) })
const connectivityColor = computed(() => store.isConnected ? 'text-green-400' : 'text-red-400') const vpnConnected = computed(() => {
const connectivityDotColor = computed(() => store.isConnected ? 'bg-green-400' : 'bg-red-400') const pkg = packages.value['tailscale']
return !!pkg && pkg.state === PackageState.Running
})
const bitcoinSyncDisplay = computed(() => {
if (!systemStats.bitcoinAvailable) return 'Not running'
if (systemStats.bitcoinSyncPercent >= 99.9) return 'Synced'
return `${systemStats.bitcoinSyncPercent.toFixed(1)}%`
})
// Quick Start // Quick Start
const quickStartDismissed = ref(false) const quickStartDismissed = ref(false)

View File

@ -116,6 +116,7 @@ import { useServerStore } from '@/stores/server'
import { rpcClient } from '@/api/rpc-client' import { rpcClient } from '@/api/rpc-client'
import { useMarketplaceApp } from '@/composables/useMarketplaceApp' import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
import { useAppLauncherStore } from '@/stores/appLauncher' import { useAppLauncherStore } from '@/stores/appLauncher'
import { useToast } from '@/composables/useToast'
import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue' import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue'
import MarketplaceFilterModal from './marketplace/MarketplaceFilterModal.vue' import MarketplaceFilterModal from './marketplace/MarketplaceFilterModal.vue'
import { import {
@ -135,6 +136,7 @@ const { t } = useI18n()
const showStagger = !marketplaceAnimationDone const showStagger = !marketplaceAnimationDone
const { setCurrentApp } = useMarketplaceApp() const { setCurrentApp } = useMarketplaceApp()
const appLauncher = useAppLauncherStore() const appLauncher = useAppLauncherStore()
const toast = useToast()
// Category state read initial value from query param (set by Discover page navigation) // Category state read initial value from query param (set by Discover page navigation)
const selectedCategory = ref((route.query.category as string) || 'all') const selectedCategory = ref((route.query.category as string) || 'all')
@ -300,11 +302,8 @@ const filteredApps = computed(() => {
) )
} }
apps.sort((a, b) => { // Hide installed and installing apps from marketplace they belong in My Apps
const aInstalled = isInstalled(a.id) ? 1 : 0 apps = apps.filter(app => !isInstalled(app.id) && !installingApps.has(app.id))
const bInstalled = isInstalled(b.id) ? 1 : 0
return aInstalled - bInstalled
})
return apps return apps
}) })
@ -433,6 +432,10 @@ async function installApp(app: MarketplaceApp) {
id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0 id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Preparing installation...', attempt: 0
}) })
// Navigate to My Apps immediately and show toast
toast.info(`Installing ${app.title ?? app.id} — check My Apps`)
router.push('/dashboard/apps').catch(() => {})
try { try {
const installUrl = app.url || app.manifestUrl || app.s9pkUrl const installUrl = app.url || app.manifestUrl || app.s9pkUrl
@ -457,6 +460,10 @@ async function installCommunityApp(app: MarketplaceApp) {
id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0 id: app.id, title: app.title ?? app.id, status: 'downloading', progress: 10, message: 'Pulling Docker image...', attempt: 0
}) })
// Navigate to My Apps immediately and show toast
toast.info(`Installing ${app.title ?? app.id} — check My Apps`)
router.push('/dashboard/apps').catch(() => {})
try { try {
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' }) installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 20, message: 'Downloading container image...' })

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col"> <div class="max-w-[800px] w-full max-h-full relative z-10 onb-scroll-container flex flex-col">
<!-- Header --> <!-- Header -->
<div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3"> <div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
<h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]"> <h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col"> <div class="max-w-[800px] w-full max-h-full relative z-10 onb-scroll-container flex flex-col">
<!-- Header --> <!-- Header -->
<div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3"> <div class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
<h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]"> <h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">
@ -69,7 +69,14 @@
</div> </div>
<!-- Fixed Footer --> <!-- Fixed Footer -->
<div class="flex-shrink-0 flex justify-center px-3 sm:px-4 pt-3 pb-4 sm:pb-6"> <div class="flex-shrink-0 flex items-center justify-center gap-4 max-w-[600px] mx-auto w-full px-6 sm:px-8 pt-3 pb-4 sm:pb-6">
<span
v-if="!restored"
@click="goBack"
class="path-action-button path-action-button--continue cursor-pointer select-none inline-flex items-center justify-center"
>
Back
</span>
<button <button
v-if="!restored" v-if="!restored"
@click="restore" @click="restore"
@ -116,6 +123,11 @@ const restoredDid = ref('')
const allFilled = computed(() => seedWords.value.every(w => w.trim().length > 0)) const allFilled = computed(() => seedWords.value.every(w => w.trim().length > 0))
function goBack() {
playNavSound('action')
router.push('/onboarding/path').catch(() => {})
}
onMounted(() => { onMounted(() => {
nextTick(() => { nextTick(() => {
setTimeout(() => wordInputs.value[0]?.focus({ preventScroll: true }), 300) setTimeout(() => wordInputs.value[0]?.focus({ preventScroll: true }), 300)

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6"> <div class="h-[100dvh] flex items-center justify-center p-3 sm:p-4 md:p-6">
<div class="max-w-[800px] w-full max-h-full relative z-10 path-glass-container onb-scroll-container flex flex-col"> <div class="max-w-[800px] w-full max-h-full relative z-10 onb-scroll-container flex flex-col">
<!-- Header (hidden after verification) --> <!-- Header (hidden after verification) -->
<div v-if="!verified" class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3"> <div v-if="!verified" class="text-center flex-shrink-0 px-3 sm:px-4 pt-4 sm:pt-6 pb-2 sm:pb-3">
<h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]"> <h1 class="text-xl sm:text-2xl md:text-[26px] font-semibold text-white/96 mb-1.5 drop-shadow-[0_2px_6px_rgba(0,0,0,0.4)]">
@ -168,7 +168,24 @@ onMounted(() => {
return return
} }
words.value = JSON.parse(stored) words.value = JSON.parse(stored)
challengeIndices.value = pickRandomIndices(4, 24)
// Restore challenge indices so going back and returning asks the same words
const savedIndices = sessionStorage.getItem('_seed_challenge_indices')
if (savedIndices) {
try {
const parsed = JSON.parse(savedIndices)
if (Array.isArray(parsed) && parsed.length === 4) {
challengeIndices.value = parsed
} else {
challengeIndices.value = pickRandomIndices(4, 24)
}
} catch {
challengeIndices.value = pickRandomIndices(4, 24)
}
} else {
challengeIndices.value = pickRandomIndices(4, 24)
}
sessionStorage.setItem('_seed_challenge_indices', JSON.stringify(challengeIndices.value))
nextTick(() => { nextTick(() => {
setTimeout(() => inputRefs.value[0]?.focus({ preventScroll: true }), 300) setTimeout(() => inputRefs.value[0]?.focus({ preventScroll: true }), 300)
@ -232,6 +249,7 @@ async function verify() {
localStorage.setItem('neode_did', res.did) localStorage.setItem('neode_did', res.did)
if (res.nostr_npub) localStorage.setItem('neode_nostr_npub', res.nostr_npub) if (res.nostr_npub) localStorage.setItem('neode_nostr_npub', res.nostr_npub)
sessionStorage.removeItem('_seed_words') sessionStorage.removeItem('_seed_words')
sessionStorage.removeItem('_seed_challenge_indices')
nextTick(() => { nextTick(() => {
setTimeout(() => continueButton.value?.focus({ preventScroll: true }), 100) setTimeout(() => continueButton.value?.focus({ preventScroll: true }), 100)

View File

@ -124,10 +124,12 @@ onMounted(async () => {
proceedToApp() proceedToApp()
return return
} }
log('server up + NOT onboarded → boot screen') log('server up + NOT onboarded → onboarding intro')
router.replace('/onboarding/intro').catch(() => {})
return
} }
// Server not ready OR first boot show boot screen // Server not ready show boot screen (waiting for backend)
showBootScreen.value = true showBootScreen.value = true
}) })
</script> </script>

View File

@ -24,7 +24,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
{ id: 'portainer', title: 'Portainer', version: '2.19.4', description: 'Container management UI. Manage your containerized services through the web.', icon: '/assets/img/app-icons/portainer.webp', author: 'Portainer', dockerImage: `${R}/portainer:latest`, repoUrl: 'https://github.com/portainer/portainer' }, { id: 'portainer', title: 'Portainer', version: '2.19.4', description: 'Container management UI. Manage your containerized services through the web.', icon: '/assets/img/app-icons/portainer.webp', author: 'Portainer', dockerImage: `${R}/portainer:latest`, repoUrl: 'https://github.com/portainer/portainer' },
{ id: 'uptime-kuma', title: 'Uptime Kuma', version: '1.23.0', description: 'Self-hosted uptime monitoring. Track HTTP, TCP, DNS, and more.', icon: '/assets/img/app-icons/uptime-kuma.webp', author: 'Uptime Kuma', dockerImage: `${R}/uptime-kuma:1`, repoUrl: 'https://github.com/louislam/uptime-kuma' }, { id: 'uptime-kuma', title: 'Uptime Kuma', version: '1.23.0', description: 'Self-hosted uptime monitoring. Track HTTP, TCP, DNS, and more.', icon: '/assets/img/app-icons/uptime-kuma.webp', author: 'Uptime Kuma', dockerImage: `${R}/uptime-kuma:1`, repoUrl: 'https://github.com/louislam/uptime-kuma' },
{ id: 'tailscale', title: 'Tailscale', version: '1.78.0', description: 'Zero-config VPN. Secure remote access with WireGuard mesh networking.', icon: '/assets/img/app-icons/tailscale.webp', author: 'Tailscale', dockerImage: `${R}/tailscale:stable`, repoUrl: 'https://github.com/tailscale/tailscale' }, { id: 'tailscale', title: 'Tailscale', version: '1.78.0', description: 'Zero-config VPN. Secure remote access with WireGuard mesh networking.', icon: '/assets/img/app-icons/tailscale.webp', author: 'Tailscale', dockerImage: `${R}/tailscale:stable`, repoUrl: 'https://github.com/tailscale/tailscale' },
{ id: 'electrumx', title: 'ElectrumX', version: '1.18.0', description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.', icon: '/assets/img/app-icons/electrumx.png', author: 'Luke Childs', dockerImage: `${R}/electrumx:v1.18.0`, repoUrl: 'https://github.com/spesmilo/electrumx' }, { id: 'electrumx', title: 'ElectrumX', version: '1.18.0', description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.', icon: '/assets/img/app-icons/electrumx.webp', author: 'Luke Childs', dockerImage: `${R}/electrumx:v1.18.0`, repoUrl: 'https://github.com/spesmilo/electrumx' },
{ id: 'fedimint', title: 'Fedimint', version: '0.10.0', description: 'Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', dockerImage: `${R}/fedimintd:v0.10.0`, repoUrl: 'https://github.com/fedimint/fedimint' }, { id: 'fedimint', title: 'Fedimint', version: '0.10.0', description: 'Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', dockerImage: `${R}/fedimintd:v0.10.0`, repoUrl: 'https://github.com/fedimint/fedimint' },
{ id: 'nostr-rs-relay', title: 'Nostr Relay', version: '0.9.0', category: 'nostr', description: 'Your own Nostr relay. Store events locally, relay for friends, publish over Tor.', icon: '/assets/img/app-icons/nostr-rs-relay.svg', author: 'scsiblade', dockerImage: `${R}/nostr-rs-relay:0.9.0`, repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' }, { id: 'nostr-rs-relay', title: 'Nostr Relay', version: '0.9.0', category: 'nostr', description: 'Your own Nostr relay. Store events locally, relay for friends, publish over Tor.', icon: '/assets/img/app-icons/nostr-rs-relay.svg', author: 'scsiblade', dockerImage: `${R}/nostr-rs-relay:0.9.0`, repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' },
{ id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'localhost/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' }, { id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'localhost/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' },

View File

@ -303,6 +303,17 @@ export function getCuratedAppList(): MarketplaceApp[] {
manifestUrl: undefined, manifestUrl: undefined,
repoUrl: 'https://github.com/immich-app/immich' repoUrl: 'https://github.com/immich-app/immich'
}, },
{
id: 'electrumx',
title: 'ElectrumX',
version: '1.18.0',
description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.',
icon: '/assets/img/app-icons/electrumx.webp',
author: 'Luke Childs',
dockerImage: `${REGISTRY}/electrumx:v1.18.0`,
manifestUrl: undefined,
repoUrl: 'https://github.com/spesmilo/electrumx'
},
{ {
id: 'filebrowser', id: 'filebrowser',
title: 'File Browser', title: 'File Browser',

View File

@ -368,6 +368,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch
if $DOCKER run -d --name bitcoin-knots --restart unless-stopped \ if $DOCKER run -d --name bitcoin-knots --restart unless-stopped \
--health-cmd="bitcoin-cli -rpcuser=\$BITCOIN_RPC_USER -rpcpassword=\$BITCOIN_RPC_PASS getblockchaininfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="bitcoin-cli -rpcuser=\$BITCOIN_RPC_USER -rpcpassword=\$BITCOIN_RPC_PASS getblockchaininfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit bitcoin-knots) --network archy-net \ --memory=$(mem_limit bitcoin-knots) --network archy-net \
--add-host host.containers.internal:host-gateway \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
--security-opt no-new-privileges:true \ --security-opt no-new-privileges:true \
-p 8332:8332 -p 8333:8333 -p 28332:28332 -p 28333:28333 \ -p 8332:8332 -p 8333:8333 -p 28332:28332 -p 28333:28333 \
@ -483,21 +484,21 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrs-ui; then
log "Starting ElectrumX UI from pre-built image..." log "Starting ElectrumX UI from pre-built image..."
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --user 0:0 \
--security-opt no-new-privileges:true \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
localhost/electrs-ui:local 2>>"$LOG" || \ localhost/electrs-ui:local 2>>"$LOG" || \
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --user 0:0 \
--security-opt no-new-privileges:true \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
electrs-ui:local 2>>"$LOG" || true electrs-ui:local 2>>"$LOG" || true
elif [ -d /opt/archipelago/docker/electrs-ui ]; then elif [ -d /opt/archipelago/docker/electrs-ui ]; then
log "Building and starting ElectrumX UI from source..." log "Building and starting ElectrumX UI from source..."
$DOCKER build -t electrs-ui:local /opt/archipelago/docker/electrs-ui 2>>"$LOG" && \ $DOCKER build -t electrs-ui:local /opt/archipelago/docker/electrs-ui 2>>"$LOG" && \
$DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \ $DOCKER run -d --name archy-electrs-ui --network host --restart unless-stopped \
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --user 0:0 \
--security-opt no-new-privileges:true \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
electrs-ui:local 2>>"$LOG" || true electrs-ui:local 2>>"$LOG" || true
else else
log "ElectrumX UI: no image or source found, skipping" log "ElectrumX UI: no image or source found, skipping"
@ -607,6 +608,7 @@ LNDCONF
$DOCKER run -d --name lnd --restart unless-stopped \ $DOCKER run -d --name lnd --restart unless-stopped \
--health-cmd="curl -sf --insecure https://localhost:8080/v1/getinfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --health-cmd="curl -sf --insecure https://localhost:8080/v1/getinfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \
--memory=$(mem_limit lnd) --network archy-net \ --memory=$(mem_limit lnd) --network archy-net \
--add-host host.containers.internal:host-gateway \
--cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE --cap-add NET_RAW \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE --cap-add NET_RAW \
--security-opt no-new-privileges:true \ --security-opt no-new-privileges:true \
-p 9735:9735 -p 10009:10009 -p 8080:8080 \ -p 9735:9735 -p 10009:10009 -p 8080:8080 \
@ -994,23 +996,23 @@ for ui in bitcoin-ui lnd-ui; do
log "Starting $ui from pre-built image..." log "Starting $ui from pre-built image..."
IMG=$($DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep "$ui" | head -1) IMG=$($DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep "$ui" | head -1)
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \ $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --user 0:0 \
--security-opt no-new-privileges:true \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
"$IMG" 2>>"$LOG" || true "$IMG" 2>>"$LOG" || true
elif [ -d "/opt/archipelago/docker/$ui" ]; then elif [ -d "/opt/archipelago/docker/$ui" ]; then
log "Building $ui from source (/opt/archipelago/docker/$ui)..." log "Building $ui from source (/opt/archipelago/docker/$ui)..."
if $DOCKER build -t "$ui:local" "/opt/archipelago/docker/$ui" 2>>"$LOG"; then if $DOCKER build -t "$ui:local" "/opt/archipelago/docker/$ui" 2>>"$LOG"; then
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \ $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --user 0:0 \
--security-opt no-new-privileges:true \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
"$ui:local" 2>>"$LOG" || true "$ui:local" 2>>"$LOG" || true
fi fi
elif [ -d "/home/archipelago/archy/docker/$ui" ]; then elif [ -d "/home/archipelago/archy/docker/$ui" ]; then
log "Building $ui from source (/home/archipelago/archy/docker/$ui)..." log "Building $ui from source (/home/archipelago/archy/docker/$ui)..."
if $DOCKER build -t "$ui:local" "/home/archipelago/archy/docker/$ui" 2>>"$LOG"; then if $DOCKER build -t "$ui:local" "/home/archipelago/archy/docker/$ui" 2>>"$LOG"; then
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \ $DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --user 0:0 \
--security-opt no-new-privileges:true \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
"$ui:local" 2>>"$LOG" || true "$ui:local" 2>>"$LOG" || true
fi fi
else else