fix: harden node upgrades and prepare 1.9.0-alpha
This commit is contained in:
@@ -366,7 +366,7 @@ function onDrop(e: DragEvent) {
|
||||
}
|
||||
|
||||
async function handleUpload(files: File[]) {
|
||||
await cloudStore.uploadFiles(files)
|
||||
await cloudStore.uploadFiles(files, route.fullPath)
|
||||
}
|
||||
|
||||
async function handleDelete(path: string) {
|
||||
|
||||
@@ -331,6 +331,7 @@ let discoverAnimationDone = false
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeStoreApps } from './apps/serviceNames'
|
||||
import AppSearchField from '@/components/AppSearchField.vue'
|
||||
import { ref, computed, onBeforeUnmount, onMounted } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
@@ -407,7 +408,7 @@ const catalogStorefront = useCachedResource<CatalogStorefront | null>({
|
||||
ttlMs: 300_000,
|
||||
persist: true,
|
||||
}).data
|
||||
const communityApps = computed(() => catalogResource.data.value ?? [])
|
||||
const communityApps = computed(() => normalizeStoreApps(catalogResource.data.value ?? []))
|
||||
const loadingCommunity = computed(() => catalogResource.entry.loadState === 'loading')
|
||||
// Keep-last-value error banner (D-07): a failed background refresh never
|
||||
// raises a toast, it only surfaces here — content stays on screen either way.
|
||||
@@ -535,9 +536,9 @@ const allApps = computed(() => {
|
||||
const nostrMerged = nostrApps.value
|
||||
.filter(app => !existingIds.has(app.id))
|
||||
.map(app => ({ ...app, category: app.category || categorizeCommunityApp(app), source: 'nostr' }))
|
||||
return [...base, ...nostrMerged]
|
||||
return normalizeStoreApps([...base, ...nostrMerged])
|
||||
}
|
||||
return base
|
||||
return normalizeStoreApps(base)
|
||||
})
|
||||
|
||||
const filteredApps = computed(() => {
|
||||
|
||||
@@ -288,7 +288,7 @@
|
||||
<WalletScanModal :show="showScanModal" @close="showScanModal = false" @sent="loadWeb5Status()" />
|
||||
<SendBitcoinModal :show="showSendModal" @close="showSendModal = false" @sent="loadWeb5Status()" @scan="showSendModal = false; showScanModal = true" />
|
||||
<ReceiveBitcoinModal :show="showReceiveModal" @close="showReceiveModal = false" @received="loadWeb5Status()" @scan="showReceiveModal = false; showScanModal = true" />
|
||||
<TransactionsModal :show="showTransactionsModal" :transactions="walletTransactions" @close="showTransactionsModal = false" />
|
||||
<TransactionsModal :show="showTransactionsModal" :transactions="walletTransactions" @updated="loadWeb5Status" @close="showTransactionsModal = false" />
|
||||
<WalletSettingsModal :show="showWalletSettingsModal" @close="showWalletSettingsModal = false" @changed="loadWeb5Status()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -50,12 +50,10 @@
|
||||
{{ section.name }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
<AppSearchField
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('marketplace.searchPlaceholder')"
|
||||
:aria-label="t('marketplace.searchApps')"
|
||||
class="app-header-search px-4 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
:label="t('marketplace.searchApps')"
|
||||
/>
|
||||
<RefreshIndicator :state="catalogResource.entry.loadState" label="Refreshing app store catalog" />
|
||||
</div>
|
||||
@@ -82,12 +80,10 @@
|
||||
type="button"
|
||||
>{{ section.name }}</button>
|
||||
</div>
|
||||
<input
|
||||
<AppSearchField
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
:placeholder="t('marketplace.searchPlaceholder')"
|
||||
:aria-label="t('marketplace.searchApps')"
|
||||
class="w-full px-4 py-3 md:py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
:label="t('marketplace.searchApps')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,6 +164,7 @@ let marketplaceAnimationDone = false
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { normalizeStoreApps } from './apps/serviceNames'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRouter, useRoute, RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -181,6 +178,7 @@ import { useCollapsingHeaderTabs } from '@/composables/useCollapsingHeaderTabs'
|
||||
import { useContainersScanTimeout } from '@/composables/useContainersScanTimeout'
|
||||
import { useCachedResource } from '@/composables/useCachedResource'
|
||||
import RefreshIndicator from '@/components/RefreshIndicator.vue'
|
||||
import AppSearchField from '@/components/AppSearchField.vue'
|
||||
import InstallVersionModal from '@/components/InstallVersionModal.vue'
|
||||
import { APP_STORE_CATEGORIES, APP_STORE_SECTIONS } from './appStoreCategories'
|
||||
import MarketplaceAppCard from './marketplace/MarketplaceAppCard.vue'
|
||||
@@ -275,7 +273,7 @@ const catalogResource = useCachedResource<MarketplaceApp[]>({
|
||||
ttlMs: 300_000,
|
||||
persist: true,
|
||||
})
|
||||
const communityApps = computed(() => catalogResource.data.value ?? [])
|
||||
const communityApps = computed(() => normalizeStoreApps(catalogResource.data.value ?? []))
|
||||
const loadingCommunity = computed(() => catalogResource.entry.loadState === 'loading')
|
||||
// Keep-last-value error banner (D-07): a failed background refresh never
|
||||
// raises a toast, it only surfaces here — content stays on screen either way.
|
||||
@@ -383,10 +381,10 @@ const allApps = computed(() => {
|
||||
const category = app.category || categorizeCommunityApp(app)
|
||||
return { ...app, category, source: 'nostr' }
|
||||
})
|
||||
return [...base, ...nostrMerged]
|
||||
return normalizeStoreApps([...base, ...nostrMerged])
|
||||
}
|
||||
|
||||
return base
|
||||
return normalizeStoreApps(base)
|
||||
})
|
||||
|
||||
const filteredApps = computed(() => {
|
||||
|
||||
@@ -89,11 +89,25 @@ describe('appsConfig service filtering', () => {
|
||||
const entries: Array<[string, PackageDataEntry]> = [
|
||||
['cuprate', makePkg('cuprate', 'Cuprate', 'money')],
|
||||
['archy-cuprate-ui', makePkg('archy-cuprate-ui', 'Cuprate UI companion', 'money')],
|
||||
['cuprate-ui', makePkg('cuprate-ui', 'Cuprate UI', 'money')],
|
||||
]
|
||||
expect(filterEntriesForTab(entries, 'apps', 'all').map(([id]) => id)).toEqual(['cuprate'])
|
||||
expect(filterEntriesForTab(entries, 'services', 'all').map(([id]) => id)).toEqual([])
|
||||
})
|
||||
|
||||
it('groups NetBird parts and BTCPay aliases without hiding a legacy-only installation', () => {
|
||||
const entries: Array<[string, PackageDataEntry]> = [
|
||||
['netbird', makePkg('netbird', 'NetBird', 'networking')],
|
||||
['netbird-dashboard', makePkg('netbird-dashboard', 'NetBird Dashboard', 'networking')],
|
||||
['netbird-server', makePkg('netbird-server', 'NetBird Server', 'networking')],
|
||||
['btcpay', makePkg('btcpay', 'BTCPay', 'money')],
|
||||
['btcpay-server', makePkg('btcpay-server', 'BTCPay Server', 'commerce')],
|
||||
]
|
||||
expect(filterEntriesForTab(entries, 'apps', 'all').map(([id]) => id)).toEqual(['netbird', 'btcpay-server'])
|
||||
expect(filterEntriesForTab(entries, 'services', 'all')).toEqual([])
|
||||
expect(filterEntriesForTab([entries[3]!], 'apps', 'all').map(([id]) => id)).toEqual(['btcpay'])
|
||||
})
|
||||
|
||||
it('falls back to packaged app icon when static icon token is not a path', () => {
|
||||
const pkg = makePkg('gitea', 'Gitea', 'dev')
|
||||
pkg['static-files']!.icon = 'git-branch'
|
||||
|
||||
@@ -35,7 +35,7 @@ const INTERNAL_TOOLING_NAMES = new Set([
|
||||
// Cuprate's dashboard is bundled as a companion of the primary cuprate
|
||||
// package; showing the generated container as a second Services entry
|
||||
// defeats the one-app presentation.
|
||||
'archy-cuprate-ui',
|
||||
'archy-cuprate-ui', 'cuprate-ui', 'netbird-dashboard', 'netbird-server',
|
||||
])
|
||||
|
||||
export function isInternalToolingPackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||
@@ -54,7 +54,7 @@ export function isServicePackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||
// Known app -> category mappings (matches App Store categorisation)
|
||||
export const APP_CATEGORY_MAP: Record<string, string> = {
|
||||
'bitcoin-core': 'money', 'bitcoin-knots': 'money', 'bitcoin-ui': 'money', 'cuprate': 'money', 'cuprate-ui': 'money', 'electrumx': 'money', 'electrs': 'money',
|
||||
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce',
|
||||
'lnd': 'money', 'mempool': 'money', 'mempool-web': 'money', 'btcpay-server': 'commerce', 'btcpay': 'commerce', 'btcpayserver': 'commerce',
|
||||
'fedimint': 'money', 'fedimint-gateway': 'money',
|
||||
'indeedhub': 'media', 'jellyfin': 'media', 'photoprism': 'media', 'immich': 'media',
|
||||
'nextcloud': 'data', 'vaultwarden': 'data', 'filebrowser': 'data', 'cryptpad': 'data',
|
||||
@@ -115,11 +115,13 @@ export function filterEntriesForTab(
|
||||
selectedCategory: string,
|
||||
): Array<[string, PackageDataEntry]> {
|
||||
const hasMempool = entries.some(([id]) => id === 'mempool')
|
||||
const hasBtcpay = entries.some(([id]) => id === 'btcpay-server')
|
||||
return entries.filter(([id, pkg]) => {
|
||||
// Older daemons can retain the frontend manifest alias during a restart.
|
||||
// Keep one tile while the updated scanner converges; a legacy-only node
|
||||
// must still be able to see and operate its sole Mempool entry.
|
||||
if (hasMempool && ['mempool-web', 'mempool-frontend', 'archy-mempool-web'].includes(id)) return false
|
||||
if (hasBtcpay && ['btcpay', 'btcpayserver'].includes(id)) return false
|
||||
if (isInternalToolingPackage(id, pkg)) return false
|
||||
const wantsWebsites = activeTab === 'websites' || activeTab === 'services'
|
||||
const isWebsite = isWebsitePackage(id, pkg)
|
||||
|
||||
@@ -29,6 +29,7 @@ export const SERVICE_NAMES = new Set([
|
||||
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
|
||||
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
|
||||
'bitcoin-ui', 'lnd-ui', 'electrs-ui',
|
||||
'cuprate-ui', 'netbird-dashboard', 'netbird-server',
|
||||
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
|
||||
'indeedhub-api', 'indeedhub-ffmpeg',
|
||||
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
|
||||
@@ -64,3 +65,15 @@ export function isServiceContainer(id: string): boolean {
|
||||
export function isStoreListedApp(id: string): boolean {
|
||||
return !isServiceContainer(id) && !NODE_INTERNAL_IDS.has(id)
|
||||
}
|
||||
|
||||
/** Normalize every catalog source, including persisted caches from older UIs. */
|
||||
export function normalizeStoreApps<T extends { id: string }>(apps: readonly T[]): T[] {
|
||||
const result = new Map<string, T>()
|
||||
for (const app of apps) {
|
||||
if (!isStoreListedApp(app.id)) continue
|
||||
const id = ['btcpay', 'btcpayserver'].includes(app.id) ? 'btcpay-server' : app.id
|
||||
// Prefer the real app's metadata to a legacy image-pin-only alias.
|
||||
if (!result.has(id) || app.id === id) result.set(id, { ...app, id })
|
||||
}
|
||||
return [...result.values()]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { signedCatalogToApps } from '../curatedApps'
|
||||
import { normalizeStoreApps } from '../../apps/serviceNames'
|
||||
|
||||
describe('one App Store card per product', () => {
|
||||
it('keeps Cuprate and NetBird components out and prefers canonical BTCPay metadata', () => {
|
||||
const entries = {
|
||||
btcpay: { version: '2.4.3' },
|
||||
'btcpay-server': { version: '2.4.3', manifest: { app: { name: 'BTCPay Server', category: 'commerce', metadata: { icon: '/btcpay.svg' } } } },
|
||||
cuprate: { version: '0.1' }, 'cuprate-ui': { version: '1.7' },
|
||||
netbird: { version: '2.38' }, 'netbird-dashboard': { version: '2.38' }, 'netbird-server': { version: '0.71' },
|
||||
}
|
||||
for (const apps of [entries, Object.fromEntries(Object.entries(entries).reverse())]) {
|
||||
const result = signedCatalogToApps({ apps })
|
||||
expect(result.map(x => x.id).sort()).toEqual(['btcpay-server', 'cuprate', 'netbird'])
|
||||
expect(result.find(x => x.id === 'btcpay-server')).toMatchObject({ title: 'BTCPay Server', category: 'commerce', icon: '/btcpay.svg', version: '2.4.3' })
|
||||
}
|
||||
})
|
||||
it('normalizes persisted/community entries repeatedly without hiding independent services', () => {
|
||||
const stale = ['cuprate-ui', 'netbird-server', 'netbird-dashboard', 'btcpay', 'btcpay-server', 'angor-indexer', 'angor-relay', 'electrumx'].map(id => ({ id }))
|
||||
const result = normalizeStoreApps(stale)
|
||||
expect(result.map(x => x.id)).toEqual(['btcpay-server', 'angor-indexer', 'angor-relay', 'electrumx'])
|
||||
expect(normalizeStoreApps(result)).toEqual(result)
|
||||
expect(normalizeStoreApps([{ id: 'btcpay' }])).toEqual([{ id: 'btcpay-server' }])
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MarketplaceApp } from './types'
|
||||
import { isStoreListedApp } from '../apps/serviceNames'
|
||||
import { isStoreListedApp, normalizeStoreApps } from '../apps/serviceNames'
|
||||
|
||||
const R = 'source.archipelago-foundation.org/lfg2025'
|
||||
|
||||
@@ -98,7 +98,7 @@ export function signedCatalogToApps(catalog: SignedAppCatalog): MarketplaceApp[]
|
||||
source: 'signed-catalog',
|
||||
})
|
||||
}
|
||||
return out
|
||||
return normalizeStoreApps(out)
|
||||
}
|
||||
|
||||
/** The daemon-verified signed catalog, kept for synchronous port-auth lookups
|
||||
@@ -192,7 +192,7 @@ const CATALOG_URLS = [
|
||||
* Caches for 1 hour. Returns null only if ALL sources fail. */
|
||||
export async function fetchAppCatalog(): Promise<AppCatalog | null> {
|
||||
// Return cache if fresh
|
||||
if (cachedCatalog && Date.now() - catalogFetchedAt < CATALOG_TTL) return cachedCatalog
|
||||
if (cachedCatalog && Date.now() - catalogFetchedAt < CATALOG_TTL) return { ...cachedCatalog, apps: normalizeStoreApps(cachedCatalog.apps) }
|
||||
|
||||
// The daemon-verified signed catalog first (release-root signature checked
|
||||
// server-side): it is what makes a newly published app appear without a
|
||||
@@ -239,16 +239,16 @@ export async function fetchAppCatalog(): Promise<AppCatalog | null> {
|
||||
// apps); signed entries fill version/image gaps and append brand-new apps.
|
||||
const byId = new Map<string, MarketplaceApp>()
|
||||
for (const app of signedApps) byId.set(app.id, app)
|
||||
for (const app of community?.apps ?? []) {
|
||||
for (const app of normalizeStoreApps(community?.apps ?? [])) {
|
||||
const existing = byId.get(app.id)
|
||||
byId.set(app.id, existing ? { ...app, version: app.version || existing.version, dockerImage: app.dockerImage || existing.dockerImage } : app)
|
||||
byId.set(app.id, existing ? { ...app, version: existing.version || app.version, dockerImage: existing.dockerImage || app.dockerImage } : app)
|
||||
}
|
||||
const merged: AppCatalog = {
|
||||
version: community?.version ?? 1,
|
||||
registry: community?.registry ?? R,
|
||||
featured: signedFeatured ?? community?.featured,
|
||||
storefront: signedStorefront ?? community?.storefront,
|
||||
apps: [...byId.values()],
|
||||
apps: normalizeStoreApps([...byId.values()]),
|
||||
}
|
||||
cachedCatalog = merged
|
||||
catalogFetchedAt = Date.now()
|
||||
@@ -261,6 +261,7 @@ export async function fetchAppCatalog(): Promise<AppCatalog | null> {
|
||||
const stored = localStorage.getItem('archy_catalog')
|
||||
if (stored) {
|
||||
cachedCatalog = JSON.parse(stored) as AppCatalog
|
||||
cachedCatalog.apps = normalizeStoreApps(cachedCatalog.apps)
|
||||
catalogFetchedAt = Date.now() - CATALOG_TTL + 5 * 60 * 1000 // re-check in 5 min
|
||||
return cachedCatalog
|
||||
}
|
||||
@@ -314,6 +315,7 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
||||
mempool: ['mempool', 'mempool-web', 'archy-mempool-web'],
|
||||
bitcoin: ['bitcoin-knots'],
|
||||
btcpay: ['btcpay-server'],
|
||||
'btcpay-server': ['btcpay-server', 'btcpay', 'btcpayserver'],
|
||||
immich: ['immich-server', 'immich-app', 'immich_server'],
|
||||
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
|
||||
fedimint: ['fedimint-gateway'],
|
||||
|
||||
@@ -72,6 +72,7 @@ export const INSTALLED_ALIASES: Record<string, string[]> = {
|
||||
mempool: ['mempool-web', 'mempool-api', 'archy-mempool-web', 'archy-mempool-db'],
|
||||
bitcoin: ['bitcoin-knots'],
|
||||
btcpay: ['btcpay-server', 'archy-btcpay-db', 'archy-nbxplorer'],
|
||||
'btcpay-server': ['btcpay-server', 'btcpay', 'btcpayserver'],
|
||||
immich: ['immich-server', 'immich-app', 'immich_server', 'immich_postgres', 'immich_redis'],
|
||||
nextcloud: ['nextcloud-aio', 'nextcloud-server'],
|
||||
fedimint: ['fedimint-gateway'],
|
||||
|
||||
@@ -362,16 +362,27 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.8.23-alpha -->
|
||||
<!-- v1.9.0-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.23-alpha</span>
|
||||
<span class="text-xs text-white/40">October 1, 2026</span>
|
||||
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.9.0-alpha</span>
|
||||
<span class="text-xs text-white/40">October 5, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Keep Cuprate and NetBird supporting components out of app listings and consolidate BTCPay Server under Commerce.</p>
|
||||
<p>Default on-chain sends, channel opens and cooperative closes to a dynamic next-block fee target, preserving explicit slower and custom choices.</p>
|
||||
<p>Add reviewed fee-bump quotes, explicit budgets and durable operation tracking for supported wallet transactions.</p>
|
||||
<p>Preserve Nginx Proxy Manager storage, same-node upstream connectivity, certificates and access controls through managed migrations.</p>
|
||||
<p>Restrict public management access while retaining configured public apps and ACME certificate validation.</p>
|
||||
<p>Serve the Mempool explorer on the Angor indexer origin alongside its API.</p>
|
||||
<p>Include the self-contained LoRa flashing tool and explicit board selection in update and installer payloads.</p>
|
||||
<p>Preserve paid-file Lightning entitlements across restarts and recover settled invoices from LND. Retry delivery without paying again and retain purchased files in the owned cache.</p>
|
||||
<p>Return explicit payment-status errors with safe retry guidance when verification is unavailable.</p>
|
||||
<p>Show compact upload progress across screens, retain the original destination, and cancel active and queued uploads.</p>
|
||||
<p>Keep upload progress on its original screen, show completion there or notify on other screens, and cancel active and queued uploads.</p>
|
||||
<p>Resume interrupted uploads while the app remains open, preserve the original destination, and verify saved file contents before reporting completion.</p>
|
||||
<p>Provision a unique private File Browser login on each node while keeping Cloud sign-in automatic and preserving existing accounts and files.</p>
|
||||
<p>Update Nostr dependencies to reject forged relay events and oversized encrypted messages; preserve native signing and encryption compatibility.</p>
|
||||
<p>Allow apps to opt in to a validated public-key list of user identities without granting signing access.</p>
|
||||
<p>Make transaction filters transparent and horizontally scrollable on mobile.</p>
|
||||
<p>Keep Immich internal services out of My Apps, avoid false recovery states for healthy stacks, and allow removal of retired catalog apps.</p>
|
||||
<p>Repair the redundant managed Portainer network override that can prevent startup, preserving custom overrides and persistent state.</p>
|
||||
|
||||
Reference in New Issue
Block a user