fix: onboarding persistence, clipboard, install UI, OnlyOffice removal, UI containers
Onboarding: - Persist current step in localStorage — page refresh resumes where user was - Router afterEach saves step; guard redirects to saved step, not always intro - Show npub alongside DID on restore success screen UI fixes: - Clipboard polyfill for HTTP contexts (fixes Copy DID crash on non-HTTPS) - AppCard installing overlay shows for pkg.state=installing (survives refresh) - Hide uninstall button during installation - Frontend version bumped to 1.3.2 App store: - OnlyOffice fully removed from marketplace, curated apps, app config - Replaced with CryptPad references throughout - Remove OnlyOffice from ISO capture patterns Container stability: - UI containers (bitcoin-ui, lnd-ui, electrs-ui) pull from registry first - Added --cap-add FOWNER for rootless Podman compatibility - electrs-ui now included in first-boot loop alongside bitcoin-ui and lnd-ui Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a808458124
commit
81b4db82d1
@ -1055,7 +1055,7 @@ IMAGES_CAPTURED_FROM_SERVER=0
|
|||||||
if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
if [ -n "$DEV_SERVER" ] && [ "$DEV_SERVER" != "localhost" ] && [ "$DEV_SERVER" != "127.0.0.1" ]; then
|
||||||
echo " Capturing container images from live server ($DEV_SERVER)..."
|
echo " Capturing container images from live server ($DEV_SERVER)..."
|
||||||
# Patterns match against `podman images` repository names (not container names)
|
# Patterns match against `podman images` repository names (not container names)
|
||||||
CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server grafana uptime-kuma jellyfin vaultwarden searxng mariadb valkey nginx-alpine portainer photoprism nextcloud nginx-proxy-manager onlyoffice adguard"
|
CAPTURE_PATTERNS="bitcoin-ui bitcoinknots lnd lnd-ui electrs-ui filebrowser mempool backend frontend electrs tailscale homeassistant home-assistant btcpayserver nbxplorer postgres alpine-tor nostr-rs-relay strfry fedimintd gatewayd dwn-server vaultwarden searxng mariadb valkey nginx-alpine portainer nginx-proxy-manager adguard"
|
||||||
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
|
REMOTE_TMP="/tmp/archipelago-image-capture-$$"
|
||||||
SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
|
SAVED_LIST=$(ssh "$DEV_SERVER" "mkdir -p $REMOTE_TMP && for p in $CAPTURE_PATTERNS; do img=\$(podman images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -i \"\$p\" | head -1); [ -n \"\$img\" ] && podman save -o \"$REMOTE_TMP/\$p.tar\" \"\$img\" 2>/dev/null && echo \"\$p\"; done" 2>/dev/null) || true
|
||||||
for p in $SAVED_LIST; do
|
for p in $SAVED_LIST; do
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.2.0-alpha",
|
"version": "1.3.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@ -29,4 +29,15 @@ export async function isOnboardingComplete(): Promise<boolean> {
|
|||||||
export async function completeOnboarding(): Promise<void> {
|
export async function completeOnboarding(): Promise<void> {
|
||||||
await callWithRetry(() => rpcClient.completeOnboarding(), 3)
|
await callWithRetry(() => rpcClient.completeOnboarding(), 3)
|
||||||
localStorage.setItem('neode_onboarding_complete', '1')
|
localStorage.setItem('neode_onboarding_complete', '1')
|
||||||
|
localStorage.removeItem('neode_onboarding_step')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Save current onboarding step so refresh resumes where user left off */
|
||||||
|
export function saveOnboardingStep(step: string): void {
|
||||||
|
localStorage.setItem('neode_onboarding_step', step)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the last saved onboarding step, or 'intro' if none */
|
||||||
|
export function getSavedOnboardingStep(): string {
|
||||||
|
return localStorage.getItem('neode_onboarding_step') || 'intro'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,24 @@ import './style.css'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import i18n from './i18n'
|
import i18n from './i18n'
|
||||||
|
|
||||||
|
// Clipboard polyfill for HTTP (non-secure) contexts where navigator.clipboard is unavailable
|
||||||
|
if (!navigator.clipboard) {
|
||||||
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
|
value: {
|
||||||
|
async writeText(text: string) {
|
||||||
|
const ta = document.createElement('textarea')
|
||||||
|
ta.value = text
|
||||||
|
ta.style.cssText = 'position:fixed;opacity:0'
|
||||||
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(ta)
|
||||||
|
},
|
||||||
|
async readText() { return '' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|||||||
@ -303,10 +303,11 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
}
|
}
|
||||||
// Check if this is a fresh install that needs onboarding
|
// Check if this is a fresh install that needs onboarding
|
||||||
try {
|
try {
|
||||||
const { isOnboardingComplete } = await import('@/composables/useOnboarding')
|
const { isOnboardingComplete, getSavedOnboardingStep } = await import('@/composables/useOnboarding')
|
||||||
const setupDone = await isOnboardingComplete()
|
const setupDone = await isOnboardingComplete()
|
||||||
if (!setupDone) {
|
if (!setupDone) {
|
||||||
next('/onboarding/intro')
|
const step = getSavedOnboardingStep()
|
||||||
|
next(`/onboarding/${step}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -328,6 +329,14 @@ router.beforeEach(async (to, _from, next) => {
|
|||||||
next()
|
next()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Persist onboarding step so page refresh resumes where user left off
|
||||||
|
router.afterEach((to) => {
|
||||||
|
const match = to.path.match(/^\/onboarding\/(.+)/)
|
||||||
|
if (match && match[1] !== 'intro') {
|
||||||
|
localStorage.setItem('neode_onboarding_step', match[1]!)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Stop all login/splash audio when entering the dashboard
|
// Stop all login/splash audio when entering the dashboard
|
||||||
router.afterEach((to, from) => {
|
router.afterEach((to, from) => {
|
||||||
if (to.path.startsWith('/dashboard') && !from.path.startsWith('/dashboard')) {
|
if (to.path.startsWith('/dashboard') && !from.path.startsWith('/dashboard')) {
|
||||||
|
|||||||
@ -39,6 +39,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="restoredNpub" class="path-option-card cursor-default px-4 py-4 sm:px-6 sm:py-5">
|
||||||
|
<div class="text-left">
|
||||||
|
<h3 class="text-xs sm:text-sm font-semibold text-white/80 mb-2 uppercase tracking-wide">Your Nostr ID</h3>
|
||||||
|
<div class="bg-black/40 rounded-lg p-3 sm:p-4 backdrop-blur-sm border border-white/10">
|
||||||
|
<p class="text-white/95 font-mono text-xs sm:text-sm break-all leading-relaxed">{{ restoredNpub }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-white/50 mt-2">For Nostr social apps and NIP-07 signing</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Word Input Grid -->
|
<!-- Word Input Grid -->
|
||||||
@ -120,6 +129,7 @@ const isRestoring = ref(false)
|
|||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const serverStarting = ref(false)
|
const serverStarting = ref(false)
|
||||||
const restoredDid = ref('')
|
const restoredDid = ref('')
|
||||||
|
const restoredNpub = ref('')
|
||||||
|
|
||||||
const allFilled = computed(() => seedWords.value.every(w => w.trim().length > 0))
|
const allFilled = computed(() => seedWords.value.every(w => w.trim().length > 0))
|
||||||
|
|
||||||
@ -167,6 +177,7 @@ async function restore() {
|
|||||||
if (res.restored) {
|
if (res.restored) {
|
||||||
restored.value = true
|
restored.value = true
|
||||||
restoredDid.value = res.did
|
restoredDid.value = res.did
|
||||||
|
restoredNpub.value = res.nostr_npub || ''
|
||||||
if (res.did) localStorage.setItem('neode_did', res.did)
|
if (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)
|
||||||
|
|
||||||
|
|||||||
@ -10,17 +10,17 @@
|
|||||||
@click="$emit('goToApp', id)"
|
@click="$emit('goToApp', id)"
|
||||||
@keydown.enter="handleEnter"
|
@keydown.enter="handleEnter"
|
||||||
>
|
>
|
||||||
<!-- Installing overlay -->
|
<!-- Installing overlay — shown for both client-tracked installs and backend 'installing' state -->
|
||||||
<div
|
<div
|
||||||
v-if="isInstalling"
|
v-if="isInstalling || pkg.state === 'installing'"
|
||||||
class="absolute inset-0 z-20 flex items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl"
|
class="absolute inset-0 z-20 flex items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-3 text-white/90">
|
<div class="flex flex-col items-center gap-3 text-white/90">
|
||||||
<svg class="animate-spin h-5 w-5" fill="none" viewBox="0 0 24 24">
|
<svg class="animate-spin h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<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>
|
<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>
|
</svg>
|
||||||
<span class="text-sm font-medium">Installing</span>
|
<span class="text-sm font-medium">{{ installProgress?.message || 'Installing...' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
<!-- Uninstall Icon (not for web-only apps) -->
|
<!-- Uninstall Icon (not for web-only apps) -->
|
||||||
<button
|
<button
|
||||||
v-if="!isWebOnly && !isUninstalling"
|
v-if="!isWebOnly && !isUninstalling && !isInstalling && pkg.state !== 'installing'"
|
||||||
@click.stop="$emit('showUninstall', id, pkg)"
|
@click.stop="$emit('showUninstall', id, pkg)"
|
||||||
class="absolute top-4 right-4 p-2 rounded-lg text-white/60 hover:text-red-400 hover:bg-red-500/20 transition-colors z-10"
|
class="absolute top-4 right-4 p-2 rounded-lg text-white/60 hover:text-red-400 hover:bg-red-500/20 transition-colors z-10"
|
||||||
:aria-label="`${t('common.uninstall')} ${pkg.manifest?.title || id}`"
|
:aria-label="`${t('common.uninstall')} ${pkg.manifest?.title || id}`"
|
||||||
|
|||||||
@ -35,7 +35,7 @@ export const APP_CATEGORY_MAP: Record<string, string> = {
|
|||||||
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce',
|
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce',
|
||||||
'fedimint': 'money', 'fedimint-gateway': 'money',
|
'fedimint': 'money', 'fedimint-gateway': 'money',
|
||||||
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
||||||
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'onlyoffice': 'data',
|
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'cryptpad': 'data',
|
||||||
'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home',
|
'homeassistant': 'home', 'lorabell': 'home', 'endurain': 'home',
|
||||||
'searxng': 'community', 'ollama': 'community', 'grafana': 'data',
|
'searxng': 'community', 'ollama': 'community', 'grafana': 'data',
|
||||||
'nostr-rs-relay': 'nostr', 'nostrudel': 'nostr',
|
'nostr-rs-relay': 'nostr', 'nostrudel': 'nostr',
|
||||||
@ -103,7 +103,7 @@ export const WEB_ONLY_APPS: Record<string, PackageDataEntry> = {
|
|||||||
export const TAB_LAUNCH_APPS = new Set([
|
export const TAB_LAUNCH_APPS = new Set([
|
||||||
'btcpay-server', 'grafana', 'photoprism', 'homeassistant',
|
'btcpay-server', 'grafana', 'photoprism', 'homeassistant',
|
||||||
'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer',
|
'vaultwarden', 'nextcloud', 'uptime-kuma', 'portainer',
|
||||||
'onlyoffice', 'nginx-proxy-manager', 'tailscale',
|
'cryptpad', 'nginx-proxy-manager', 'tailscale',
|
||||||
])
|
])
|
||||||
|
|
||||||
export function opensInTab(id: string): boolean {
|
export function opensInTab(id: string): boolean {
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
{ id: 'grafana', title: 'Grafana', version: '10.2.0', description: 'Analytics and monitoring platform. Dashboards for your node metrics and system health.', icon: '/assets/img/app-icons/grafana.png', author: 'Grafana Labs', dockerImage: `${R}/grafana:10.2.0`, repoUrl: 'https://github.com/grafana/grafana' },
|
{ id: 'grafana', title: 'Grafana', version: '10.2.0', description: 'Analytics and monitoring platform. Dashboards for your node metrics and system health.', icon: '/assets/img/app-icons/grafana.png', author: 'Grafana Labs', dockerImage: `${R}/grafana:10.2.0`, repoUrl: 'https://github.com/grafana/grafana' },
|
||||||
{ id: 'searxng', title: 'SearXNG', version: '2024.1.0', description: 'Privacy-respecting metasearch engine. Search the internet without being tracked or profiled.', icon: '/assets/img/app-icons/searxng.png', author: 'SearXNG', dockerImage: `${R}/searxng:latest`, repoUrl: 'https://github.com/searxng/searxng' },
|
{ id: 'searxng', title: 'SearXNG', version: '2024.1.0', description: 'Privacy-respecting metasearch engine. Search the internet without being tracked or profiled.', icon: '/assets/img/app-icons/searxng.png', author: 'SearXNG', dockerImage: `${R}/searxng:latest`, repoUrl: 'https://github.com/searxng/searxng' },
|
||||||
{ id: 'ollama', title: 'Ollama', version: '0.5.4', description: 'Run AI models locally. Llama, Mistral, and more — on your hardware, completely private.', icon: '/assets/img/app-icons/ollama.png', author: 'Ollama', dockerImage: `${R}/ollama:latest`, repoUrl: 'https://github.com/ollama/ollama' },
|
{ id: 'ollama', title: 'Ollama', version: '0.5.4', description: 'Run AI models locally. Llama, Mistral, and more — on your hardware, completely private.', icon: '/assets/img/app-icons/ollama.png', author: 'Ollama', dockerImage: `${R}/ollama:latest`, repoUrl: 'https://github.com/ollama/ollama' },
|
||||||
{ id: 'onlyoffice', title: 'OnlyOffice', version: '7.5.1', description: 'Self-hosted office suite. Documents, spreadsheets, and presentations without the cloud.', icon: '/assets/img/app-icons/onlyoffice.webp', author: 'Ascensio System SIA', dockerImage: `${R}/onlyoffice:latest`, repoUrl: 'https://github.com/ONLYOFFICE/DocumentServer' },
|
{ id: 'cryptpad', title: 'CryptPad', version: '2024.12.0', description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.', icon: '/assets/img/app-icons/cryptpad.webp', author: 'XWiki SAS', dockerImage: `${R}/cryptpad:2024.12.0`, repoUrl: 'https://github.com/cryptpad/cryptpad' },
|
||||||
{ id: 'penpot', title: 'Penpot', version: '2.4', description: 'Open-source design platform. Self-hosted alternative to Figma for design and prototyping.', icon: '/assets/img/app-icons/penpot.webp', author: 'Penpot', dockerImage: `${R}/penpot-frontend:2.4`, repoUrl: 'https://github.com/penpot/penpot' },
|
{ id: 'penpot', title: 'Penpot', version: '2.4', description: 'Open-source design platform. Self-hosted alternative to Figma for design and prototyping.', icon: '/assets/img/app-icons/penpot.webp', author: 'Penpot', dockerImage: `${R}/penpot-frontend:2.4`, repoUrl: 'https://github.com/penpot/penpot' },
|
||||||
{ id: 'nextcloud', title: 'Nextcloud', version: '28', description: 'Your own private cloud. File sync, calendars, contacts — all on your hardware.', icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud', dockerImage: `${R}/nextcloud:28`, repoUrl: 'https://github.com/nextcloud/server' },
|
{ id: 'nextcloud', title: 'Nextcloud', version: '28', description: 'Your own private cloud. File sync, calendars, contacts — all on your hardware.', icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud', dockerImage: `${R}/nextcloud:28`, repoUrl: 'https://github.com/nextcloud/server' },
|
||||||
{ id: 'vaultwarden', title: 'Vaultwarden', version: '1.30.0', description: 'Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.', icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', dockerImage: `${R}/vaultwarden:1.30.0-alpine`, repoUrl: 'https://github.com/dani-garcia/vaultwarden' },
|
{ id: 'vaultwarden', title: 'Vaultwarden', version: '1.30.0', description: 'Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.', icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', dockerImage: `${R}/vaultwarden:1.30.0-alpine`, repoUrl: 'https://github.com/dani-garcia/vaultwarden' },
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { shortDid } from './utils'
|
import { shortDid } from './utils'
|
||||||
|
import { safeClipboardWrite } from '../web5/utils'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
selfDid: string
|
selfDid: string
|
||||||
@ -59,7 +60,7 @@ const shortDidDisplay = computed(() => shortDid(props.selfDid))
|
|||||||
|
|
||||||
function handleCopy() {
|
function handleCopy() {
|
||||||
if (props.selfDid) {
|
if (props.selfDid) {
|
||||||
navigator.clipboard.writeText(props.selfDid).catch(() => {})
|
safeClipboardWrite(props.selfDid)
|
||||||
didCopied.value = true
|
didCopied.value = true
|
||||||
setTimeout(() => { didCopied.value = false }, 2000)
|
setTimeout(() => { didCopied.value = false }, 2000)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -227,15 +227,15 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
repoUrl: 'https://github.com/ollama/ollama'
|
repoUrl: 'https://github.com/ollama/ollama'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'onlyoffice',
|
id: 'cryptpad',
|
||||||
title: 'OnlyOffice',
|
title: 'CryptPad',
|
||||||
version: '7.5.1',
|
version: '2024.12.0',
|
||||||
description: 'Office suite for document collaboration. Edit docs, spreadsheets, and presentations.',
|
description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.',
|
||||||
icon: '/assets/img/app-icons/onlyoffice.webp',
|
icon: '/assets/img/app-icons/cryptpad.webp',
|
||||||
author: 'Ascensio System SIA',
|
author: 'XWiki SAS',
|
||||||
dockerImage: `${REGISTRY}/onlyoffice:latest`,
|
dockerImage: `${REGISTRY}/cryptpad:2024.12.0`,
|
||||||
manifestUrl: undefined,
|
manifestUrl: undefined,
|
||||||
repoUrl: 'https://github.com/ONLYOFFICE/DocumentServer'
|
repoUrl: 'https://github.com/cryptpad/cryptpad'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'penpot',
|
id: 'penpot',
|
||||||
|
|||||||
@ -37,18 +37,21 @@ export function formatMessageTime(ts: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function safeClipboardWrite(text: string): Promise<void> {
|
export async function safeClipboardWrite(text: string): Promise<void> {
|
||||||
if (navigator.clipboard?.writeText) {
|
// navigator.clipboard is unavailable on HTTP (non-secure contexts)
|
||||||
await navigator.clipboard.writeText(text)
|
try {
|
||||||
} else {
|
if (navigator.clipboard?.writeText) {
|
||||||
const ta = document.createElement('textarea')
|
await navigator.clipboard.writeText(text)
|
||||||
ta.value = text
|
return
|
||||||
ta.style.position = 'fixed'
|
}
|
||||||
ta.style.opacity = '0'
|
} catch { /* fall through to textarea fallback */ }
|
||||||
document.body.appendChild(ta)
|
const ta = document.createElement('textarea')
|
||||||
ta.select()
|
ta.value = text
|
||||||
document.execCommand('copy')
|
ta.style.position = 'fixed'
|
||||||
document.body.removeChild(ta)
|
ta.style.opacity = '0'
|
||||||
}
|
document.body.appendChild(ta)
|
||||||
|
ta.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(ta)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMediaType(mime: string): boolean {
|
export function isMediaType(mime: string): boolean {
|
||||||
|
|||||||
@ -1092,41 +1092,35 @@ if [ -f "$RPC_PASS_FILE" ]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for ui in bitcoin-ui lnd-ui; do
|
for ui in bitcoin-ui lnd-ui electrs-ui; do
|
||||||
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "$ui"; then
|
if $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q "$ui"; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
case $ui in
|
case $ui in
|
||||||
# UI containers use --network host so they can proxy to localhost services
|
# UI containers use --network host so they can proxy to localhost services
|
||||||
# (Bitcoin RPC at 127.0.0.1:8332, backend at 127.0.0.1:5678)
|
|
||||||
# Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=8080 (host 8081)
|
# Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=8080 (host 8081)
|
||||||
bitcoin-ui) PORT_ARG=""; NET_ARG="--network host" ;;
|
bitcoin-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${BITCOIN_UI_IMAGE}" ;;
|
||||||
lnd-ui) PORT_ARG="-p 8081:8080"; NET_ARG="" ;; # nginx inside listens on 8080 (no NET_BIND_SERVICE needed)
|
lnd-ui) PORT_ARG="-p 8081:8080"; NET_ARG=""; REG_IMG="${LND_UI_IMAGE}" ;;
|
||||||
electrs-ui) PORT_ARG=""; NET_ARG="--network host" ;;
|
electrs-ui) PORT_ARG=""; NET_ARG="--network host"; REG_IMG="${ELECTRS_UI_IMAGE}" ;;
|
||||||
esac
|
esac
|
||||||
CONTAINER_NAME="archy-$ui"
|
CONTAINER_NAME="archy-$ui"
|
||||||
if $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$ui"; then
|
UI_CAPS="--user 0:0 --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE"
|
||||||
log "Starting $ui from pre-built image..."
|
|
||||||
|
# Try registry image first, then local image, then build from source
|
||||||
|
if [ -n "$REG_IMG" ] && $DOCKER pull --tls-verify=false "$REG_IMG" 2>>"$LOG"; then
|
||||||
|
log "Starting $ui from registry ($REG_IMG)..."
|
||||||
|
$DOCKER run -d --name "$CONTAINER_NAME" $PORT_ARG --restart unless-stopped --memory=$(mem_limit "$CONTAINER_NAME") $NET_ARG \
|
||||||
|
$UI_CAPS "$REG_IMG" 2>>"$LOG" || true
|
||||||
|
elif $DOCKER images --format '{{.Repository}}:{{.Tag}}' 2>/dev/null | grep -q "$ui"; then
|
||||||
|
log "Starting $ui from local 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 \
|
||||||
--user 0:0 \
|
$UI_CAPS "$IMG" 2>>"$LOG" || true
|
||||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
|
||||||
"$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..."
|
||||||
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 \
|
||||||
--user 0:0 \
|
$UI_CAPS "$ui:local" 2>>"$LOG" || true
|
||||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
|
||||||
"$ui:local" 2>>"$LOG" || true
|
|
||||||
fi
|
|
||||||
elif [ -d "/home/archipelago/archy/docker/$ui" ]; then
|
|
||||||
log "Building $ui from source (/home/archipelago/archy/docker/$ui)..."
|
|
||||||
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 \
|
|
||||||
--user 0:0 \
|
|
||||||
--cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \
|
|
||||||
"$ui:local" 2>>"$LOG" || true
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log "$ui: no image or source found, skipping"
|
log "$ui: no image or source found, skipping"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user