Merge branch 'bitcoin-version-bulletproof' into mesh-multiversion-integration
This commit is contained in:
@@ -207,6 +207,15 @@
|
||||
<p class="text-white/60 text-xl mt-4 font-mono">// Cypherpunks write code. We run nodes.</p>
|
||||
</div>
|
||||
|
||||
<!-- First-install version chooser (Bitcoin Knots / Core) -->
|
||||
<InstallVersionModal
|
||||
:show="showInstallModal"
|
||||
:app-id="installModalApp?.id || ''"
|
||||
:app="installModalApp"
|
||||
@close="showInstallModal = false; installModalApp = null"
|
||||
@confirm="onInstallModalConfirm"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -228,6 +237,7 @@ import { APP_STORE_SECTIONS } from './appStoreCategories'
|
||||
import DiscoverHero from './discover/DiscoverHero.vue'
|
||||
import FeaturedApps from './discover/FeaturedApps.vue'
|
||||
import AppGrid from './discover/AppGrid.vue'
|
||||
import InstallVersionModal from '@/components/InstallVersionModal.vue'
|
||||
import type { MarketplaceApp, FeaturedApp } from './discover/types'
|
||||
import { getCuratedAppList, INSTALLED_ALIASES, FEATURED_DEFINITIONS, categorizeCommunityApp, fetchAppCatalog, type CatalogFeatured } from './discover/curatedApps'
|
||||
|
||||
@@ -262,6 +272,10 @@ const appStoreSections = computed(() => APP_STORE_SECTIONS)
|
||||
// been removed in favour of the store's phase-aware mapping.
|
||||
const installingApps = serverStore.installingApps
|
||||
|
||||
// First-install version-choice modal (multi-version apps: Bitcoin Knots / Core)
|
||||
const showInstallModal = ref(false)
|
||||
const installModalApp = ref<MarketplaceApp | null>(null)
|
||||
|
||||
function navigateToMarketplace(categoryId: string) {
|
||||
router.push({ name: 'marketplace', query: { category: categoryId } })
|
||||
}
|
||||
@@ -427,19 +441,42 @@ function launchInstalledApp(app: MarketplaceApp) {
|
||||
appLauncher.openSession(app.id)
|
||||
}
|
||||
|
||||
function handleInstall(app: MarketplaceApp) {
|
||||
async function handleInstall(app: MarketplaceApp) {
|
||||
const blocked = installBlockedReason(app.id)
|
||||
if (blocked) {
|
||||
toast.error(blocked)
|
||||
return
|
||||
}
|
||||
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||
// Multi-version apps (Bitcoin Knots / Core): let the runner pick a version up
|
||||
// front via a full-screen modal (latest pre-selected) instead of silently
|
||||
// installing the default. Best-effort — if the lookup fails we install directly.
|
||||
try {
|
||||
const info = await rpcClient.getPackageVersions(app.id)
|
||||
if (info.supportsVersions && info.versions.length > 1) {
|
||||
installModalApp.value = app
|
||||
showInstallModal.value = true
|
||||
return
|
||||
}
|
||||
} catch { /* no catalog versions — fall through to direct install */ }
|
||||
startInstall(app)
|
||||
}
|
||||
|
||||
function startInstall(app: MarketplaceApp, versionOverride?: string) {
|
||||
if (app.source === 'local') {
|
||||
installApp(app)
|
||||
installApp(app, versionOverride)
|
||||
} else {
|
||||
installCommunityApp(app)
|
||||
installCommunityApp(app, versionOverride)
|
||||
}
|
||||
}
|
||||
|
||||
function onInstallModalConfirm(version: string) {
|
||||
const app = installModalApp.value
|
||||
showInstallModal.value = false
|
||||
installModalApp.value = null
|
||||
if (app) startInstall(app, version)
|
||||
}
|
||||
|
||||
function viewAppDetails(app: MarketplaceApp) {
|
||||
try {
|
||||
if (isInstalled(app.id)) {
|
||||
@@ -514,27 +551,27 @@ function failInstall(app: MarketplaceApp, err: unknown) {
|
||||
trackTimeout(() => { serverStore.clearInstallProgress(app.id) }, 5000)
|
||||
}
|
||||
|
||||
async function installApp(app: MarketplaceApp) {
|
||||
async function installApp(app: MarketplaceApp, versionOverride?: string) {
|
||||
if (installingApps.has(app.id) || isInstalled(app.id)) return
|
||||
queueInstall(app)
|
||||
toast.info("Installing " + (app.title ?? app.id) + " - check My Apps")
|
||||
router.push('/dashboard/apps').catch(() => {})
|
||||
try {
|
||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version }, timeout: 600000 })
|
||||
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: versionOverride || app.version }, timeout: 600000 })
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.error('Installation failed:', err)
|
||||
failInstall(app, err)
|
||||
}
|
||||
}
|
||||
|
||||
async function installCommunityApp(app: MarketplaceApp) {
|
||||
async function installCommunityApp(app: MarketplaceApp, versionOverride?: string) {
|
||||
if (installingApps.has(app.id) || isInstalled(app.id) || !app.dockerImage) return
|
||||
queueInstall(app)
|
||||
toast.info("Installing " + (app.title ?? app.id) + " - check My Apps")
|
||||
router.push('/dashboard/apps').catch(() => {})
|
||||
try {
|
||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: app.version }
|
||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: versionOverride || app.version }
|
||||
if ((app as Record<string, unknown>).containerConfig) {
|
||||
installParams.containerConfig = (app as Record<string, unknown>).containerConfig
|
||||
}
|
||||
|
||||
@@ -48,11 +48,9 @@
|
||||
<select
|
||||
v-model="selectedVersion"
|
||||
:disabled="versionBusy"
|
||||
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm focus:outline-none focus:border-blue-400/60"
|
||||
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white pl-3 pr-9 py-2 text-sm focus:outline-none focus:border-blue-400/60"
|
||||
>
|
||||
<option v-for="v in versionInfo.versions" :key="v.version" :value="v.version">
|
||||
{{ $ver(v.version) }}{{ v.default ? ' — latest' : '' }}{{ v.deprecated ? ' (deprecated)' : '' }}{{ v.eol ? ` · EOL ${v.eol}` : '' }}
|
||||
</option>
|
||||
<option v-for="v in versionInfo.versions" :key="v.version" :value="v.version">{{ versionOptionLabel(v) }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -252,7 +250,8 @@
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { AppCredentialsResponse } from '@/types/api'
|
||||
import { rpcClient, type PackageVersionsResponse } from '../../api/rpc-client'
|
||||
import { rpcClient, type PackageVersionsResponse, type CatalogVersionInfo } from '../../api/rpc-client'
|
||||
import { displayVersion } from '@/utils/version'
|
||||
|
||||
const { t } = useI18n()
|
||||
const copiedCredential = ref('')
|
||||
@@ -334,10 +333,33 @@ const isPinned = computed(() => !!versionInfo.value?.pinnedVersion)
|
||||
const versionDirty = computed(() => {
|
||||
const info = versionInfo.value
|
||||
if (!info) return false
|
||||
const currentSelection = info.pinnedVersion || info.default || info.installedVersion || ''
|
||||
return selectedVersion.value !== currentSelection || autoUpdate.value !== info.autoUpdate
|
||||
return selectedVersion.value !== pickSelection(info) || autoUpdate.value !== info.autoUpdate
|
||||
})
|
||||
|
||||
// Option label: the floating "latest" entry reads as a sentence (no "v"
|
||||
// prefix); every concrete version is normalized via $ver + status suffixes.
|
||||
function versionOptionLabel(v: CatalogVersionInfo): string {
|
||||
if (v.version === 'latest') return t('appDetails.alwaysUseLatestVersion')
|
||||
let label = displayVersion(v.version)
|
||||
if (v.deprecated) label += ' (deprecated)'
|
||||
if (v.eol) label += ` · EOL ${v.eol}`
|
||||
return label
|
||||
}
|
||||
|
||||
// Resolve the dropdown's current value and GUARANTEE it matches a real option,
|
||||
// otherwise the native <select> renders blank. Prefer the pin, then the catalog
|
||||
// default, then the running version — but fall back to the default/first option
|
||||
// when none of those is actually in the list (e.g. a stale installedVersion the
|
||||
// catalog no longer carries, which is what left the control blank).
|
||||
function pickSelection(info: PackageVersionsResponse): string {
|
||||
const options = info.versions.map((v) => v.version)
|
||||
const preferred = info.pinnedVersion || info.default || info.installedVersion || ''
|
||||
if (preferred && options.includes(preferred)) return preferred
|
||||
return info.default && options.includes(info.default)
|
||||
? info.default
|
||||
: info.versions[0]?.version || ''
|
||||
}
|
||||
|
||||
async function loadVersions(appId: string) {
|
||||
versionInfo.value = null
|
||||
versionError.value = ''
|
||||
@@ -346,7 +368,7 @@ async function loadVersions(appId: string) {
|
||||
const info = await rpcClient.getPackageVersions(appId)
|
||||
if (!info.supportsVersions || !info.versions.length) return
|
||||
versionInfo.value = info
|
||||
selectedVersion.value = info.pinnedVersion || info.default || info.installedVersion || info.versions[0]?.version || ''
|
||||
selectedVersion.value = pickSelection(info)
|
||||
autoUpdate.value = info.autoUpdate
|
||||
} catch (err) {
|
||||
if (import.meta.env.DEV) console.warn('[AppSidebar] getPackageVersions failed:', err)
|
||||
@@ -381,7 +403,7 @@ function cancelDowngrade() {
|
||||
downgradeWarning.value = ''
|
||||
// Reset the dropdown to the current selection.
|
||||
const info = versionInfo.value
|
||||
if (info) selectedVersion.value = info.pinnedVersion || info.default || info.installedVersion || ''
|
||||
if (info) selectedVersion.value = pickSelection(info)
|
||||
}
|
||||
|
||||
watch(
|
||||
|
||||
Reference in New Issue
Block a user