feat: dynamic app catalog, Gitea app polish, registry sync
App catalog served from Gitea repos (app-catalog) with 35 apps. Nodes fetch catalog dynamically — new apps appear without frontend rebuild. Test app added and removed to verify pipeline. Gitea manifest updated with internal_port/nginx_proxy for iframe. Updated catalog.json, nginx configs, app session configs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -506,7 +506,12 @@ async function installCommunityApp(app: MarketplaceApp) {
|
||||
router.push('/dashboard/apps').catch(() => {})
|
||||
try {
|
||||
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 })
|
||||
// Pass containerConfig from catalog if available (allows dynamic apps without hardcoded backend config)
|
||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: app.version }
|
||||
if ((app as Record<string, unknown>).containerConfig) {
|
||||
installParams.containerConfig = (app as Record<string, unknown>).containerConfig
|
||||
}
|
||||
await rpcClient.call({ method: 'package.install', params: installParams, timeout: 180000 })
|
||||
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' })
|
||||
startInstallPolling(app.id, 'Initializing application...')
|
||||
} catch (err) {
|
||||
|
||||
@@ -78,6 +78,13 @@
|
||||
|
||||
<!-- Action buttons (right side) -->
|
||||
<div class="gamepad-actions">
|
||||
<button
|
||||
class="action-btn action-c"
|
||||
@touchstart.prevent="down('c')"
|
||||
@touchend.prevent="up('c')"
|
||||
@touchcancel.prevent="up('c')"
|
||||
aria-label="Special"
|
||||
>C</button>
|
||||
<button
|
||||
class="action-btn action-b"
|
||||
@touchstart.prevent="down('b')"
|
||||
@@ -226,17 +233,17 @@ function tap(key: string) { send(key, 'down'); setTimeout(() => send(key, 'up'),
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* ── Action buttons (A / B) ── */
|
||||
/* ── Action buttons (A / B / C) ── */
|
||||
.gamepad-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
@@ -267,4 +274,13 @@ function tap(key: string) { send(key, 'down'); setTimeout(() => send(key, 'up'),
|
||||
.action-b:active {
|
||||
background: rgba(96, 165, 250, 0.45);
|
||||
}
|
||||
|
||||
.action-c {
|
||||
background: rgba(74, 222, 128, 0.2);
|
||||
border-color: rgba(74, 222, 128, 0.5);
|
||||
color: #4ade80;
|
||||
}
|
||||
.action-c:active {
|
||||
background: rgba(74, 222, 128, 0.45);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -51,9 +51,7 @@ export const APP_PORTS: Record<string, number> = {
|
||||
/** Apps that need nginx proxy for iframe embedding.
|
||||
* IndeedHub loads via /app/indeedhub/ proxy for nostr-provider.js injection
|
||||
* from the container's internal nginx so iframe works on all servers. */
|
||||
export const PROXY_APPS: Record<string, string> = {
|
||||
'gitea': '/app/gitea/',
|
||||
}
|
||||
export const PROXY_APPS: Record<string, string> = {}
|
||||
|
||||
/** Nginx proxy paths -- used on HTTPS to avoid mixed content (HTTPS parent + HTTP port iframe).
|
||||
* On HTTP, direct port access is used instead (faster, no proxy). */
|
||||
|
||||
@@ -19,27 +19,58 @@ export interface AppCatalog {
|
||||
}
|
||||
|
||||
let cachedCatalog: AppCatalog | null = null
|
||||
let catalogFetchedAt = 0
|
||||
const CATALOG_TTL = 60 * 60 * 1000 // 1 hour cache
|
||||
|
||||
/** Fetch catalog.json (served by nginx, can be updated independently of the build).
|
||||
* Returns null if fetch fails — caller should fall back to hardcoded list. */
|
||||
/** Remote catalog URLs — tried in order. First success wins. */
|
||||
const CATALOG_URLS = [
|
||||
// Primary: Gitea raw file (dynamic, updated without frontend rebuild)
|
||||
'https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json',
|
||||
// Fallback: direct IP if DNS fails
|
||||
'http://23.182.128.160:3000/lfg2025/app-catalog/raw/branch/main/catalog.json',
|
||||
// Last resort: local static file (baked into frontend build)
|
||||
'/catalog.json',
|
||||
]
|
||||
|
||||
/** Fetch app catalog from remote registry, with local fallback.
|
||||
* Caches for 1 hour. Returns null only if ALL sources fail. */
|
||||
export async function fetchAppCatalog(): Promise<AppCatalog | null> {
|
||||
if (cachedCatalog) return cachedCatalog
|
||||
try {
|
||||
const res = await fetch('/catalog.json', { signal: AbortSignal.timeout(5000) })
|
||||
if (!res.ok) return null
|
||||
const data = await res.json() as AppCatalog
|
||||
// Expand short docker image refs to full registry paths
|
||||
const registry = data.registry || R
|
||||
for (const app of data.apps) {
|
||||
if (app.dockerImage && !app.dockerImage.includes('/')) {
|
||||
app.dockerImage = `${registry}/${app.dockerImage}`
|
||||
// Return cache if fresh
|
||||
if (cachedCatalog && Date.now() - catalogFetchedAt < CATALOG_TTL) return cachedCatalog
|
||||
|
||||
for (const url of CATALOG_URLS) {
|
||||
try {
|
||||
const res = await fetch(url, { signal: AbortSignal.timeout(5000) })
|
||||
if (!res.ok) continue
|
||||
const data = await res.json() as AppCatalog
|
||||
if (!data.apps?.length) continue
|
||||
|
||||
// Expand short docker image refs to full registry paths
|
||||
const registry = data.registry || R
|
||||
for (const app of data.apps) {
|
||||
if (app.dockerImage && !app.dockerImage.includes('/')) {
|
||||
app.dockerImage = `${registry}/${app.dockerImage}`
|
||||
}
|
||||
}
|
||||
}
|
||||
cachedCatalog = data
|
||||
return data
|
||||
} catch {
|
||||
return null
|
||||
cachedCatalog = data
|
||||
catalogFetchedAt = Date.now()
|
||||
// Cache in localStorage for offline fallback
|
||||
try { localStorage.setItem('archy_catalog', JSON.stringify(data)) } catch {}
|
||||
return data
|
||||
} catch { continue }
|
||||
}
|
||||
|
||||
// Try localStorage cache as final fallback
|
||||
try {
|
||||
const stored = localStorage.getItem('archy_catalog')
|
||||
if (stored) {
|
||||
cachedCatalog = JSON.parse(stored) as AppCatalog
|
||||
catalogFetchedAt = Date.now() - CATALOG_TTL + 5 * 60 * 1000 // re-check in 5 min
|
||||
return cachedCatalog
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// ---------- Hardcoded fallback (used when catalog.json is unavailable) ----------
|
||||
@@ -75,6 +106,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
||||
{ id: 'routstr', title: 'Routstr', version: '0.4.3', category: 'community', description: 'Decentralized AI inference proxy. Pay-per-request with Cashu ecash, provider discovery via Nostr.', icon: '/assets/img/app-icons/routstr.svg', author: 'Routstr', dockerImage: `${R}/routstr:v0.4.3`, repoUrl: 'https://github.com/routstr/routstr-core' },
|
||||
{ id: 'nostrudel', title: 'noStrudel', version: '0.40.0', category: 'nostr', description: 'Feature-rich Nostr web client. Browse feeds, post notes, manage relays with NIP-07.', icon: '/assets/img/app-icons/nostrudel.svg', author: 'hzrd149', dockerImage: '', repoUrl: 'https://github.com/hzrd149/nostrudel', webUrl: 'https://nostrudel.ninja' },
|
||||
{ id: 'botfights', title: 'BotFights', version: '1.0.0', category: 'community', description: 'Bot arena + 2-player arcade fighter with controller support. AI bots battle in trivia, humans duke it out with controllers.', icon: '/assets/img/app-icons/botfights.svg', author: 'BotFights', dockerImage: `${R}/botfights:1.1.0`, repoUrl: 'https://botfights.net' },
|
||||
{ id: 'gitea', title: 'Gitea', version: '1.23', category: 'development', description: 'Self-hosted Git service with container registry, CI/CD, issue tracking, and package hosting.', icon: '/assets/img/app-icons/gitea.svg', author: 'Gitea', dockerImage: 'docker.io/gitea/gitea:1.23', repoUrl: 'https://gitea.com' },
|
||||
{ id: 'nwnn', title: 'Next Web News Network', version: '1.0.0', category: 'l484', description: 'Decentralized news aggregator. Community-curated Bitcoin and sovereignty content.', icon: '/assets/img/app-icons/nwnn.png', author: 'L484', dockerImage: '', repoUrl: 'https://nwnn.l484.com', webUrl: 'https://nwnn.l484.com' },
|
||||
{ id: '484-kitchen', title: '484 Kitchen', version: '1.0.0', category: 'l484', description: 'K484 application platform for the L484 network.', icon: '/assets/img/app-icons/484-kitchen.png', author: 'L484', dockerImage: '', repoUrl: 'https://484.kitchen', webUrl: 'https://484.kitchen' },
|
||||
{ id: 'call-the-operator', title: 'Call the Operator', version: '1.0.0', category: 'l484', description: 'Escape the Matrix — explore decentralized alternatives and reclaim sovereignty.', icon: '/assets/img/app-icons/call-the-operator.png', author: 'TX1138', dockerImage: '', repoUrl: 'https://cta.tx1138.com', webUrl: 'https://cta.tx1138.com' },
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
import type { MarketplaceAppInfo } from '@/composables/useMarketplaceApp'
|
||||
|
||||
/** Container config that can be passed from the remote catalog to the backend
|
||||
* for apps not hardcoded in config.rs */
|
||||
export interface ContainerConfig {
|
||||
ports?: string[]
|
||||
volumes?: string[]
|
||||
env?: string[]
|
||||
}
|
||||
|
||||
export type MarketplaceApp = Partial<MarketplaceAppInfo> & {
|
||||
id: string
|
||||
trustScore?: number
|
||||
trustTier?: string
|
||||
relayCount?: number
|
||||
containerConfig?: ContainerConfig
|
||||
requires?: string[]
|
||||
tier?: string
|
||||
}
|
||||
|
||||
export type FeaturedApp = MarketplaceApp & {
|
||||
|
||||
Reference in New Issue
Block a user