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:
Dorian
2026-04-12 08:20:18 -04:00
parent 1147dbd882
commit 3078d4b69e
18 changed files with 892 additions and 142 deletions

View File

@@ -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) {

View File

@@ -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>

View File

@@ -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). */

View File

@@ -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' },

View File

@@ -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 & {