feat: botfights container app + mobile gamepad + indeedhub fixes
Some checks failed
Build Archipelago ISO (dev) / build-iso (push) Has been cancelled

- 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

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