feat: botfights container app + mobile gamepad + indeedhub fixes

- Promote botfights from external proxy to container app (port 9100)
- Add /app/botfights/ nginx proxy rules (HTTP + HTTPS)
- Add ARCHY_EMBEDDED env var to botfights container config
- Add BOTFIGHTS_IMAGE to image-versions.sh
- Add mobile gamepad overlay (D-pad + A/B + START/SELECT) for botfights
  arcade mode, sends postMessage arcade-input to iframe
- Remove old /ext/botfights/ and port 8901 external proxy blocks
- IndeeHub: add post-install nginx patching for NIP-07 provider injection
- IndeeHub: fix docker image references to registry (was localhost)
- IndeeHub: update port 7777 -> 7778

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian 2026-04-11 16:47:54 -04:00
parent 1807ceeebd
commit e19094739b
11 changed files with 401 additions and 110 deletions

View File

@ -877,6 +877,7 @@ pub(super) async fn get_app_config(
"PORT=9100".to_string(),
format!("JWT_SECRET={}", jwt_secret),
"FIGHT_LOOP_ENABLED=true".to_string(),
"ARCHY_EMBEDDED=1".to_string(),
],
None,
None,

View File

@ -897,6 +897,108 @@ autopilot.active=false\n",
}
}
// IndeeHub: inject nostr-provider.js and patch container nginx for NIP-07 signing
if package_id == "indeedhub" {
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
// 1. Remove X-Frame-Options so iframe embedding works
let _ = tokio::process::Command::new("podman")
.args(["exec", "indeedhub", "sed", "-i", "/X-Frame-Options/d",
"/etc/nginx/conf.d/default.conf"])
.output()
.await;
// 2. Copy nostr-provider.js into container
let provider_src = "/opt/archipelago/web-ui/nostr-provider.js";
if tokio::fs::metadata(provider_src).await.is_ok() {
let _ = tokio::process::Command::new("podman")
.args(["cp", provider_src, "indeedhub:/usr/share/nginx/html/nostr-provider.js"])
.output()
.await;
}
// 3. Add nostr-provider.js location block + sub_filter injection
let check = tokio::process::Command::new("podman")
.args(["exec", "indeedhub", "grep", "-q", "nostr-provider",
"/etc/nginx/conf.d/default.conf"])
.output()
.await;
let already_patched = check.map(|o| o.status.success()).unwrap_or(false);
if !already_patched {
// Read current nginx config from container
let cat_out = tokio::process::Command::new("podman")
.args(["exec", "indeedhub", "cat", "/etc/nginx/conf.d/default.conf"])
.output()
.await;
if let Ok(out) = cat_out {
if out.status.success() {
let conf = String::from_utf8_lossy(&out.stdout).to_string();
// Insert provider location block before the sw.js location
let conf = conf.replace(
"location = /sw.js {",
"location = /nostr-provider.js {\n\
\x20 add_header Cache-Control \"no-cache, no-store, must-revalidate\";\n\
\x20 expires off;\n\
\x20 }\n\n\
\x20 location = /sw.js {"
);
// Inject script tag into HTML via sub_filter
let conf = if conf.contains("try_files") && !conf.contains("sub_filter") {
conf.replacen(
"try_files $uri $uri/ /index.html;",
"try_files $uri $uri/ /index.html;\n\
\x20 sub_filter_once on;\n\
\x20 sub_filter '</head>' '<script src=\"/nostr-provider.js\"></script></head>';",
1,
)
} else {
conf
};
// Write patched config back into container
let tmp_path = "/tmp/indeedhub-nginx-patch.conf";
if tokio::fs::write(tmp_path, &conf).await.is_ok() {
let _ = tokio::process::Command::new("podman")
.args(["cp", tmp_path, "indeedhub:/etc/nginx/conf.d/default.conf"])
.output()
.await;
let _ = tokio::fs::remove_file(tmp_path).await;
}
}
}
}
// 4. Fix X-Forwarded-Prefix for NIP-98 URL reconstruction in iframe context
let _ = tokio::process::Command::new("podman")
.args(["exec", "indeedhub", "sed", "-i",
"s|proxy_set_header X-Forwarded-Prefix /api;|proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix/api;|",
"/etc/nginx/conf.d/default.conf"])
.output()
.await;
// 5. Reload nginx to apply changes
let reload = tokio::process::Command::new("podman")
.args(["exec", "indeedhub", "nginx", "-s", "reload"])
.output()
.await;
match reload {
Ok(o) if o.status.success() => {
info!("IndeeHub: NIP-07 provider injected, nginx patched and reloaded");
}
Ok(o) => {
tracing::warn!("IndeeHub nginx reload failed: {}",
String::from_utf8_lossy(&o.stderr));
}
Err(e) => {
tracing::warn!("IndeeHub nginx reload error: {}", e);
}
}
}
if package_id == "nextcloud" {
let host_ip = &self.config.host_ip;
// Wait for Nextcloud to finish first-run initialization

View File

@ -5,34 +5,6 @@
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# BotFights (botfights.net) → port 8901
server {
listen 8901;
server_name _;
location / {
set $upstream_botfights https://botfights.net;
proxy_pass $upstream_botfights;
proxy_http_version 1.1;
proxy_set_header Host botfights.net;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Accept-Encoding "";
proxy_ssl_server_name on;
proxy_ssl_name botfights.net;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
proxy_redirect https://botfights.net/ /;
sub_filter_once off;
sub_filter_types text/html text/css application/javascript;
}
}
# 484 Kitchen (484.kitchen) → port 8902
server {
listen 8902;

View File

@ -444,6 +444,39 @@ server {
sub_filter "src='/" "src='/app/indeedhub/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/botfights/api/ {
proxy_pass http://127.0.0.1:9100/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /app/botfights/ {
proxy_pass http://127.0.0.1:9100/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_hide_header Cross-Origin-Embedder-Policy;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Cross-Origin-Resource-Policy;
add_header X-Content-Type-Options "nosniff" always;
proxy_set_header Accept-Encoding "";
sub_filter_types text/css application/javascript application/json;
sub_filter_once off;
sub_filter 'href="/' 'href="/app/botfights/';
sub_filter 'src="/' 'src="/app/botfights/';
sub_filter "href='/" "href='/app/botfights/";
sub_filter "src='/" "src='/app/botfights/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/lnd/ {
proxy_pass http://127.0.0.1:8081/;
proxy_http_version 1.1;
@ -681,30 +714,6 @@ server {
# External site proxies — strip X-Frame-Options so iframe embedding works.
# add_header here prevents inheritance of server-level X-Frame-Options.
location /ext/botfights/ {
set $upstream_2 "https://botfights.net/";
proxy_pass $upstream_2;
proxy_http_version 1.1;
proxy_set_header Host botfights.net;
proxy_set_header Accept-Encoding "";
proxy_ssl_server_name on;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_hide_header Cross-Origin-Embedder-Policy;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Cross-Origin-Resource-Policy;
add_header X-Content-Type-Options "nosniff" always;
sub_filter_once off;
sub_filter_types text/css application/javascript;
sub_filter 'href="/' 'href="/ext/botfights/';
sub_filter 'src="/' 'src="/ext/botfights/';
sub_filter 'action="/' 'action="/ext/botfights/';
sub_filter "href='/" "href='/ext/botfights/";
sub_filter "src='/" "src='/ext/botfights/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /ext/484-kitchen/ {
set $upstream_3 "https://484.kitchen/";
@ -1081,30 +1090,6 @@ server {
# External site proxies — strip X-Frame-Options so iframe embedding works.
# add_header here prevents inheritance of server-level X-Frame-Options.
location /ext/botfights/ {
set $upstream_7 "https://botfights.net/";
proxy_pass $upstream_7;
proxy_http_version 1.1;
proxy_set_header Host botfights.net;
proxy_set_header Accept-Encoding "";
proxy_ssl_server_name on;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_hide_header Cross-Origin-Embedder-Policy;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Cross-Origin-Resource-Policy;
add_header X-Content-Type-Options "nosniff" always;
sub_filter_once off;
sub_filter_types text/css application/javascript;
sub_filter 'href="/' 'href="/ext/botfights/';
sub_filter 'src="/' 'src="/ext/botfights/';
sub_filter 'action="/' 'action="/ext/botfights/';
sub_filter "href='/" "href='/ext/botfights/";
sub_filter "src='/" "src='/ext/botfights/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /ext/484-kitchen/ {
set $upstream_8 "https://484.kitchen/";
@ -1187,33 +1172,6 @@ server {
# External site reverse proxies — each on its own port so SPAs work at root.
# Strips X-Frame-Options to allow iframe embedding from Archipelago UI.
# Injects NIP-07 nostr-provider.js for Nostr login integration.
server {
listen 8901;
server_name _;
location / {
set $upstream_11 "https://botfights.net";
proxy_pass $upstream_11;
proxy_http_version 1.1;
proxy_set_header Host botfights.net;
proxy_set_header Accept-Encoding "";
proxy_ssl_server_name on;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
add_header X-Content-Type-Options "nosniff" always;
proxy_hide_header Cross-Origin-Embedder-Policy;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Cross-Origin-Resource-Policy;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
sub_filter_once on;
}
# Serve nostr-provider.js from the main web-ui directory
location = /nostr-provider.js {
alias /opt/archipelago/web-ui/nostr-provider.js;
}
}
server {
listen 8902;
server_name _;

View File

@ -261,6 +261,38 @@ location /app/bitcoin-ui/ {
sub_filter_once on;
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/botfights/api/ {
proxy_pass http://127.0.0.1:9100/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
location /app/botfights/ {
proxy_pass http://127.0.0.1:9100/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_hide_header X-Frame-Options;
add_header X-Frame-Options "SAMEORIGIN" always;
proxy_hide_header Content-Security-Policy;
proxy_hide_header Cross-Origin-Embedder-Policy;
proxy_hide_header Cross-Origin-Opener-Policy;
proxy_hide_header Cross-Origin-Resource-Policy;
proxy_set_header Accept-Encoding "";
sub_filter_types text/css application/javascript application/json;
sub_filter_once off;
sub_filter 'href="/' 'href="/app/botfights/';
sub_filter 'src="/' 'src="/app/botfights/';
sub_filter "href='/" "href='/app/botfights/";
sub_filter "src='/" "src='/app/botfights/";
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
}
location /app/electrumx/ {
proxy_pass http://127.0.0.1:50002/;
proxy_http_version 1.1;

View File

@ -38,8 +38,13 @@
@open-new-tab-and-back="openNewTabAndBack"
/>
<!-- Mobile bottom browser bar part of flex layout, doesn't overlay content -->
<div class="md:hidden app-session-mobile-bar">
<!-- Mobile: gamepad for botfights, browser bar for everything else -->
<MobileGamepad
v-if="isMobile && appId === 'botfights'"
:iframe-ref="iframeRef ?? null"
:player="1"
/>
<div v-else class="md:hidden app-session-mobile-bar">
<button class="app-session-bar-btn" aria-label="Back" @click="iframeGoBack">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
@ -81,6 +86,7 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
import AppSessionHeader from './appSession/AppSessionHeader.vue'
import AppSessionFrame from './appSession/AppSessionFrame.vue'
import MobileGamepad from './appSession/MobileGamepad.vue'
import {
type DisplayMode, DISPLAY_MODE_KEY, NEW_TAB_APPS, IFRAME_BLOCKED_APPS,
resolveAppUrl, resolveAppTitle,

View File

@ -0,0 +1,216 @@
<template>
<!-- Mobile gamepad overlay NES-styled D-pad + action buttons.
Sends postMessage({ type: 'arcade-input', key, player, action }) to iframe. -->
<div class="mobile-gamepad">
<!-- D-Pad (left side) -->
<div class="gamepad-dpad">
<button
class="dpad-btn dpad-up"
@touchstart.prevent="down('ArrowUp')"
@touchend.prevent="up('ArrowUp')"
@touchcancel.prevent="up('ArrowUp')"
aria-label="Up"
>
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4l-6 8h12z"/></svg>
</button>
<button
class="dpad-btn dpad-left"
@touchstart.prevent="down('ArrowLeft')"
@touchend.prevent="up('ArrowLeft')"
@touchcancel.prevent="up('ArrowLeft')"
aria-label="Left"
>
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 12l8-6v12z"/></svg>
</button>
<div class="dpad-center" />
<button
class="dpad-btn dpad-right"
@touchstart.prevent="down('ArrowRight')"
@touchend.prevent="up('ArrowRight')"
@touchcancel.prevent="up('ArrowRight')"
aria-label="Right"
>
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20 12l-8-6v12z"/></svg>
</button>
<button
class="dpad-btn dpad-down"
@touchstart.prevent="down('ArrowDown')"
@touchend.prevent="up('ArrowDown')"
@touchcancel.prevent="up('ArrowDown')"
aria-label="Down"
>
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 20l6-8H6z"/></svg>
</button>
</div>
<!-- Center: START / SELECT -->
<div class="gamepad-meta">
<button
class="meta-btn"
@touchstart.prevent="tap('Escape')"
aria-label="Select"
>SEL</button>
<button
class="meta-btn"
@touchstart.prevent="tap('Enter')"
aria-label="Start"
>START</button>
</div>
<!-- Action buttons (right side) -->
<div class="gamepad-actions">
<button
class="action-btn action-b"
@touchstart.prevent="down('b')"
@touchend.prevent="up('b')"
@touchcancel.prevent="up('b')"
aria-label="Kick"
>B</button>
<button
class="action-btn action-a"
@touchstart.prevent="down('a')"
@touchend.prevent="up('a')"
@touchcancel.prevent="up('a')"
aria-label="Punch"
>A</button>
</div>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{
iframeRef: HTMLIFrameElement | null
player?: number
}>()
function send(key: string, action: 'down' | 'up') {
props.iframeRef?.contentWindow?.postMessage(
{ type: 'arcade-input', key, player: props.player ?? 1, action },
'*'
)
}
function down(key: string) { send(key, 'down') }
function up(key: string) { send(key, 'up') }
function tap(key: string) { send(key, 'down'); setTimeout(() => send(key, 'up'), 80) }
</script>
<style scoped>
.mobile-gamepad {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: 12px 20px;
padding-bottom: calc(12px + var(--safe-area-bottom, env(safe-area-inset-bottom, 0px)));
background: rgba(0, 0, 0, 0.85);
border-top: 1px solid rgba(255, 255, 255, 0.06);
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
/* ── D-Pad ── */
.gamepad-dpad {
display: grid;
grid-template-columns: 48px 48px 48px;
grid-template-rows: 48px 48px 48px;
gap: 2px;
flex-shrink: 0;
}
.dpad-btn {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
color: rgba(255, 255, 255, 0.7);
transition: background 0.1s;
}
.dpad-btn:active {
background: rgba(251, 146, 60, 0.3);
color: white;
}
.dpad-btn svg {
width: 20px;
height: 20px;
}
.dpad-up { grid-column: 2; grid-row: 1; }
.dpad-left { grid-column: 1; grid-row: 2; }
.dpad-center {
grid-column: 2;
grid-row: 2;
background: rgba(255, 255, 255, 0.03);
border-radius: 4px;
}
.dpad-right { grid-column: 3; grid-row: 2; }
.dpad-down { grid-column: 2; grid-row: 3; }
/* ── Meta buttons (START / SELECT) ── */
.gamepad-meta {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.meta-btn {
padding: 6px 16px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.45);
font-size: 10px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
}
.meta-btn:active {
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.8);
}
/* ── Action buttons (A / B) ── */
.gamepad-actions {
display: flex;
gap: 12px;
align-items: center;
flex-shrink: 0;
}
.action-btn {
width: 60px;
height: 60px;
border-radius: 50%;
font-size: 18px;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid;
transition: background 0.1s, transform 0.1s;
}
.action-btn:active {
transform: scale(0.92);
}
.action-a {
background: rgba(251, 146, 60, 0.2);
border-color: rgba(251, 146, 60, 0.5);
color: #fb923c;
}
.action-a:active {
background: rgba(251, 146, 60, 0.45);
}
.action-b {
background: rgba(96, 165, 250, 0.2);
border-color: rgba(96, 165, 250, 0.5);
color: #60a5fa;
}
.action-b:active {
background: rgba(96, 165, 250, 0.45);
}
</style>

View File

@ -41,14 +41,14 @@ export const APP_PORTS: Record<string, number> = {
'nostr-vpn': 8201,
'fips': 8202,
'routstr': 8200,
'indeedhub': 7777,
'indeedhub': 7778,
'botfights': 9100,
'dwn': 3100,
'endurain': 8080,
}
/** Apps that need nginx proxy for iframe embedding.
* IndeedHub loads via direct port 7777 -- deploy script removes X-Frame-Options
* 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> = {}
@ -87,6 +87,7 @@ export const HTTPS_PROXY_PATHS: Record<string, string> = {
'penpot': '/app/penpot/',
'grafana': '/app/grafana/',
'indeedhub': '/app/indeedhub/',
'botfights': '/app/botfights/',
'routstr': '/app/routstr/',
'nostr-vpn': '/app/nostr-vpn/',
'fips': '/app/fips/',
@ -143,7 +144,7 @@ export function resolveAppUrl(id: string, routeQueryPath?: string): string {
const proxyPath = PROXY_APPS[id]
if (proxyPath) return `${window.location.origin}${proxyPath}`
// IndeedHub: always direct port (X-Frame-Options removed by deploy script)
// IndeedHub: direct port access (nostr-provider.js baked into container image)
if (id === 'indeedhub') {
const port = APP_PORTS[id]
if (port) {

View File

@ -27,7 +27,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
{ id: 'electrumx', title: 'ElectrumX', version: '1.18.0', description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.', icon: '/assets/img/app-icons/electrumx.webp', author: 'Luke Childs', dockerImage: `${R}/electrumx:v1.18.0`, repoUrl: 'https://github.com/spesmilo/electrumx' },
{ id: 'fedimint', title: 'Fedimint', version: '0.10.0', description: 'Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', dockerImage: `${R}/fedimintd:v0.10.0`, repoUrl: 'https://github.com/fedimint/fedimint' },
{ id: 'nostr-rs-relay', title: 'Nostr Relay', version: '0.9.0', category: 'nostr', description: 'Your own Nostr relay. Store events locally, relay for friends, publish over Tor.', icon: '/assets/img/app-icons/nostr-rs-relay.svg', author: 'scsiblade', dockerImage: `${R}/nostr-rs-relay:0.9.0`, repoUrl: 'https://sr.ht/~gheartsfield/nostr-rs-relay/' },
{ id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'localhost/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' },
{ id: 'indeedhub', title: 'Indeehub', version: '0.1.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: 'git.tx1138.com/lfg2025/indeedhub:latest', repoUrl: 'https://github.com/indeedhub/indeedhub' },
{ id: 'dwn', title: 'Decentralized Web Node', version: '0.4.0', description: 'Own your data with DID-based access control. Sync across devices, sovereign.', icon: '/assets/img/app-icons/dwn.svg', author: 'TBD', dockerImage: `${R}/dwn-server:main`, repoUrl: 'https://github.com/TBD54566975/dwn-server' },
{ id: 'nostr-vpn', title: 'Nostr VPN', version: '0.3.7', category: 'networking', description: 'Tailscale-style mesh VPN with Nostr control plane. Peer discovery and key exchange over relays, WireGuard tunnels.', icon: '/assets/img/app-icons/nostr-vpn.svg', author: 'Martti Malmi', dockerImage: `${R}/nostr-vpn:v0.3.7`, repoUrl: 'https://github.com/mmalmi/nostr-vpn' },
{ id: 'fips', title: 'FIPS', version: '0.1.0', category: 'networking', description: 'Free Internetworking Peering System. Self-organizing encrypted mesh network with Nostr identity.', icon: '/assets/img/app-icons/fips.svg', author: 'Jim Corgan', dockerImage: `${R}/fips:v0.1.0`, repoUrl: 'https://github.com/jmcorgan/fips' },

View File

@ -390,7 +390,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
description: 'Bitcoin documentary streaming platform with Nostr identity sign-in. Stream God Bless Bitcoin and other educational content about sovereignty and decentralized technology.',
icon: '/assets/img/app-icons/indeedhub.png',
author: 'Indeehub Team',
dockerImage: 'localhost/indeedhub:latest',
dockerImage: 'git.tx1138.com/lfg2025/indeedhub:latest',
manifestUrl: undefined,
repoUrl: 'https://github.com/indeedhub/indeedhub'
},

View File

@ -71,6 +71,9 @@ FIPS_UI_IMAGE="$ARCHY_REGISTRY/fips-ui:latest"
# AI / Routing
ROUTSTR_IMAGE="$ARCHY_REGISTRY/routstr:v0.4.3"
# Community / Gaming
BOTFIGHTS_IMAGE="$ARCHY_REGISTRY/botfights:1.0.0"
# IndeedHub stack (local builds use :local tag, not :latest)
MINIO_IMAGE="$ARCHY_REGISTRY/minio:RELEASE.2024-11-07T00-52-20Z"
INDEEDHUB_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:16.13-alpine"