chore: release v1.7.57-alpha

This commit is contained in:
archipelago
2026-05-17 17:30:04 -04:00
parent a322b04021
commit 7804223152
37 changed files with 382 additions and 119 deletions
+13
View File
@@ -141,6 +141,7 @@ import AppHeroSection from './appDetails/AppHeroSection.vue'
import AppContentSection from './appDetails/AppContentSection.vue'
import AppSidebar from './appDetails/AppSidebar.vue'
import { resolveAppUrl } from './appSession/appSessionConfig'
import { isWebsitePackage, resolveRuntimeLaunchUrl } from './apps/appsConfig'
import {
WEB_ONLY_APP_URLS,
PACKAGE_ALIASES,
@@ -294,6 +295,18 @@ function launchApp() {
return
}
if (isWebsitePackage(id, pkg.value)) {
const url = resolveRuntimeLaunchUrl(pkg.value)
if (url) window.open(url, '_blank', 'noopener,noreferrer')
return
}
const runtimeUrl = resolveRuntimeLaunchUrl(pkg.value)
if (runtimeUrl) {
useAppLauncherStore().open({ url: runtimeUrl, title: pkg.value.manifest.title })
return
}
// Container apps should launch through session routing so protocol/path
// handling stays centralized in appSessionConfig.
useAppLauncherStore().openSession(id)
+17 -4
View File
@@ -7,7 +7,7 @@
<div class="mode-switcher flex-shrink-0">
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'apps' }" @click="activeTab = 'apps'; router.replace({ query: {} })">My Apps</button>
<RouterLink to="/dashboard/discover" class="mode-switcher-btn">App Store</RouterLink>
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'services' }" @click="activeTab = 'services'; router.replace({ query: { tab: 'services' } })">Services</button>
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'websites' }" @click="activeTab = 'websites'; router.replace({ query: { tab: 'websites' } })">Websites</button>
</div>
<div v-if="activeTab === 'apps' && categoriesWithApps.length > 1" class="mode-switcher flex-shrink-0">
<button
@@ -155,7 +155,7 @@ import AppIconGrid from './apps/AppIconGrid.vue'
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
import { useAppsActions } from './apps/useAppsActions'
import {
filterEntriesForTab, isWebOnlyApp,
type AppsTab, filterEntriesForTab, isWebOnlyApp, isWebsitePackage, opensInTab, resolveRuntimeLaunchUrl,
WEB_ONLY_APPS, buildAllCategories, useCategoriesWithApps,
} from './apps/appsConfig'
@@ -170,8 +170,8 @@ const actions = useAppsActions()
const showStagger = !appsAnimationDone
// Tabs
const activeTab = ref<'apps' | 'services'>(
route.query.tab === 'services' ? 'services' : 'apps'
const activeTab = ref<AppsTab>(
route.query.tab === 'websites' || route.query.tab === 'services' ? 'websites' : 'apps'
)
// Search (debounced)
@@ -285,6 +285,19 @@ function goToApp(id: string) {
}
function launchApp(id: string) {
const pkg = packages.value[id]
if (pkg && isWebsitePackage(id, pkg)) {
const url = resolveRuntimeLaunchUrl(pkg)
if (url) window.open(url, '_blank', 'noopener,noreferrer')
return
}
if (pkg && opensInTab(id)) {
const url = resolveRuntimeLaunchUrl(pkg)
if (url) {
window.open(url, '_blank', 'noopener,noreferrer')
return
}
}
useAppLauncherStore().openSession(id)
}
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="mode-switcher flex-shrink-0">
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
<RouterLink to="/dashboard/apps?tab=services" class="mode-switcher-btn">Services</RouterLink>
<RouterLink to="/dashboard/apps?tab=websites" class="mode-switcher-btn">Websites</RouterLink>
</div>
<div class="mode-switcher flex-shrink-0">
<RouterLink
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="mode-switcher flex-shrink-0">
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
<RouterLink to="/dashboard/apps?tab=services" class="mode-switcher-btn">Services</RouterLink>
<RouterLink to="/dashboard/apps?tab=websites" class="mode-switcher-btn">Websites</RouterLink>
</div>
<div class="mode-switcher flex-shrink-0">
<RouterLink
@@ -83,7 +83,7 @@ export const APP_URLS: Record<string, { dev: string; prod: string }> = {
'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' },
'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' },
'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' },
'nginx-proxy-manager': { dev: 'http://localhost:81', prod: 'http://localhost:81' },
'nginx-proxy-manager': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' },
'gitea': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' },
'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' },
'uptime-kuma': { dev: 'http://localhost:3002', prod: 'http://localhost:3002' },
@@ -31,7 +31,7 @@ export const APP_PORTS: Record<string, number> = {
'immich': 2283,
'immich_server': 2283,
'filebrowser': 8083,
'nginx-proxy-manager': 81,
'nginx-proxy-manager': 8081,
'gitea': 3001,
'portainer': 9000,
'tailscale': 8240,
+9 -2
View File
@@ -79,7 +79,7 @@ import { useServerStore } from '@/stores/server'
import { useAppLauncherStore } from '@/stores/appLauncher'
import type { PackageDataEntry } from '@/types/api'
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
import { canLaunch, handleImageError, opensInTab, resolveAppIcon } from './appsConfig'
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl } from './appsConfig'
import { getCuratedAppList } from '../discover/curatedApps'
const ITEMS_PER_PAGE = 16 // 4 columns x 4 rows
@@ -120,8 +120,15 @@ function getIcon(id: string, pkg: PackageDataEntry): string {
function handleTap(id: string, pkg: PackageDataEntry) {
if (canLaunch(pkg)) {
if (isWebsitePackage(id, pkg)) {
const url = resolveRuntimeLaunchUrl(pkg)
if (url) {
window.open(url, '_blank', 'noopener,noreferrer')
return
}
}
if (opensInTab(id)) {
const appUrl = resolveAppUrl(id)
const appUrl = resolveRuntimeLaunchUrl(pkg) || resolveAppUrl(id)
if (appUrl) {
window.open(appUrl, '_blank', 'noopener,noreferrer')
return
+39 -4
View File
@@ -5,6 +5,8 @@ import { computed } from 'vue'
import { PackageState, type PackageDataEntry } from '@/types/api'
import { resolveAppUrl } from '../appSession/appSessionConfig'
export type AppsTab = 'apps' | 'websites' | 'services'
// Service container name patterns (backend/infra, not user-facing)
export const SERVICE_NAMES = new Set([
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
@@ -22,6 +24,15 @@ export const SERVICE_NAMES = new Set([
'syntropy-institute', 't-zero', 'arch-presentation',
])
const INTERNAL_TOOLING_NAMES = new Set([
'buildx_buildkit_default',
])
export function isInternalToolingPackage(id: string, pkg?: PackageDataEntry): boolean {
const manifestId = pkg?.manifest?.id || ''
return INTERNAL_TOOLING_NAMES.has(id) || INTERNAL_TOOLING_NAMES.has(manifestId) || id.startsWith('buildx_buildkit') || manifestId.startsWith('buildx_buildkit')
}
export function isServiceContainer(id: string): boolean {
if (SERVICE_NAMES.has(id)) return true
if (id.startsWith('indeedhub-build_')) return true
@@ -58,14 +69,32 @@ export function getAppCategory(id: string, pkg: PackageDataEntry): string {
return cat || 'other'
}
export function runtimeLanAddress(pkg: PackageDataEntry): string {
return pkg.installed?.['interface-addresses']?.main?.['lan-address'] || ''
}
export function isKnownApp(id: string, pkg?: PackageDataEntry): boolean {
const manifestId = pkg?.manifest?.id
return !!(APP_CATEGORY_MAP[id] || (manifestId && APP_CATEGORY_MAP[manifestId]) || isWebOnlyApp(id))
}
export function isWebsitePackage(id: string, pkg?: PackageDataEntry): boolean {
if (isInternalToolingPackage(id, pkg)) return false
if (isServicePackage(id, pkg)) return true
if (isKnownApp(id, pkg)) return false
return !!pkg && !!runtimeLanAddress(pkg)
}
export function filterEntriesForTab(
entries: Array<[string, PackageDataEntry]>,
activeTab: 'apps' | 'services',
activeTab: AppsTab,
selectedCategory: string,
): Array<[string, PackageDataEntry]> {
return entries.filter(([id, pkg]) => {
const isSvc = isServicePackage(id, pkg)
if (activeTab === 'services' ? !isSvc : isSvc) return false
if (isInternalToolingPackage(id, pkg)) return false
const wantsWebsites = activeTab === 'websites' || activeTab === 'services'
const isWebsite = isWebsitePackage(id, pkg)
if (wantsWebsites ? !isWebsite : isWebsite) return false
if (activeTab === 'apps' && selectedCategory !== 'all') {
return getAppCategory(id, pkg) === selectedCategory
}
@@ -151,6 +180,12 @@ export function canLaunch(pkg: PackageDataEntry): boolean {
return !!hasUI && pkg.state === 'running' && pkg.health !== 'starting' && pkg.health !== 'unhealthy'
}
export function resolveRuntimeLaunchUrl(pkg: PackageDataEntry): string {
const addr = runtimeLanAddress(pkg)
if (!addr || typeof window === 'undefined') return addr
return addr.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, `http://${window.location.hostname}`)
}
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {
if (state === PackageState.Running && health === 'starting') return 'bg-yellow-500/20 text-yellow-200'
if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-500/20 text-orange-200'
@@ -212,7 +247,7 @@ export function useCategoriesWithApps(
allCategories: Ref<Array<{ id: string; name: string }>>,
) {
return computed(() => {
const entries = Object.entries(packages.value).filter(([id, pkg]) => !isServicePackage(id, pkg))
const entries = Object.entries(packages.value).filter(([id, pkg]) => !isWebsitePackage(id, pkg) && !isInternalToolingPackage(id, pkg))
return allCategories.value.filter(cat => {
if (cat.id === 'all') return true
return entries.some(([id, pkg]) => getAppCategory(id, pkg) === cat.id)
@@ -10,7 +10,7 @@
<RouterLink
to="/dashboard/apps"
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')) && route.query.tab !== 'services' }"
:class="{ 'mode-switcher-btn-active': (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')) && route.query.tab !== 'services' && route.query.tab !== 'websites' }"
@click.prevent="router.push({ path: '/dashboard/apps', query: {} })"
>My Apps</RouterLink>
<RouterLink
@@ -19,10 +19,10 @@
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/') || route.path === '/dashboard/discover' }"
>App Store</RouterLink>
<RouterLink
to="/dashboard/apps?tab=services"
to="/dashboard/apps?tab=websites"
class="mode-switcher-btn"
:class="{ 'mode-switcher-btn-active': route.query.tab === 'services' }"
>Services</RouterLink>
:class="{ 'mode-switcher-btn-active': route.query.tab === 'services' || route.query.tab === 'websites' }"
>Websites</RouterLink>
</div>
</div>
@@ -96,8 +96,8 @@ export function useRouteTransitions() {
// Mobile: Horizontal slide transitions between sub-tabs
if (typeof window !== 'undefined' && window.innerWidth < 768) {
const isServices = currentPath === '/dashboard/apps' && currentRoute.query.tab === 'services'
const wasServices = previousTab === 'services'
const isServices = currentPath === '/dashboard/apps' && (currentRoute.query.tab === 'services' || currentRoute.query.tab === 'websites')
const wasServices = previousTab === 'services' || previousTab === 'websites'
const currentAppsIdx = isServices ? 2
: currentPath === '/dashboard/marketplace' ? 1
: currentPath === '/dashboard/apps' ? 0 : -1