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:
parent
1807ceeebd
commit
e19094739b
@ -877,6 +877,7 @@ pub(super) async fn get_app_config(
|
|||||||
"PORT=9100".to_string(),
|
"PORT=9100".to_string(),
|
||||||
format!("JWT_SECRET={}", jwt_secret),
|
format!("JWT_SECRET={}", jwt_secret),
|
||||||
"FIGHT_LOOP_ENABLED=true".to_string(),
|
"FIGHT_LOOP_ENABLED=true".to_string(),
|
||||||
|
"ARCHY_EMBEDDED=1".to_string(),
|
||||||
],
|
],
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
|||||||
@ -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" {
|
if package_id == "nextcloud" {
|
||||||
let host_ip = &self.config.host_ip;
|
let host_ip = &self.config.host_ip;
|
||||||
// Wait for Nextcloud to finish first-run initialization
|
// Wait for Nextcloud to finish first-run initialization
|
||||||
|
|||||||
@ -5,34 +5,6 @@
|
|||||||
resolver 1.1.1.1 8.8.8.8 valid=300s;
|
resolver 1.1.1.1 8.8.8.8 valid=300s;
|
||||||
resolver_timeout 5s;
|
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
|
# 484 Kitchen (484.kitchen) → port 8902
|
||||||
server {
|
server {
|
||||||
listen 8902;
|
listen 8902;
|
||||||
|
|||||||
@ -444,6 +444,39 @@ server {
|
|||||||
sub_filter "src='/" "src='/app/indeedhub/";
|
sub_filter "src='/" "src='/app/indeedhub/";
|
||||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
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/ {
|
location /app/lnd/ {
|
||||||
proxy_pass http://127.0.0.1:8081/;
|
proxy_pass http://127.0.0.1:8081/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@ -681,30 +714,6 @@ server {
|
|||||||
|
|
||||||
# External site proxies — strip X-Frame-Options so iframe embedding works.
|
# External site proxies — strip X-Frame-Options so iframe embedding works.
|
||||||
# add_header here prevents inheritance of server-level X-Frame-Options.
|
# 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/ {
|
location /ext/484-kitchen/ {
|
||||||
set $upstream_3 "https://484.kitchen/";
|
set $upstream_3 "https://484.kitchen/";
|
||||||
|
|
||||||
@ -1081,30 +1090,6 @@ server {
|
|||||||
|
|
||||||
# External site proxies — strip X-Frame-Options so iframe embedding works.
|
# External site proxies — strip X-Frame-Options so iframe embedding works.
|
||||||
# add_header here prevents inheritance of server-level X-Frame-Options.
|
# 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/ {
|
location /ext/484-kitchen/ {
|
||||||
set $upstream_8 "https://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.
|
# 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.
|
# Strips X-Frame-Options to allow iframe embedding from Archipelago UI.
|
||||||
# Injects NIP-07 nostr-provider.js for Nostr login integration.
|
# 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 {
|
server {
|
||||||
listen 8902;
|
listen 8902;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|||||||
@ -261,6 +261,38 @@ location /app/bitcoin-ui/ {
|
|||||||
sub_filter_once on;
|
sub_filter_once on;
|
||||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
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/ {
|
location /app/electrumx/ {
|
||||||
proxy_pass http://127.0.0.1:50002/;
|
proxy_pass http://127.0.0.1:50002/;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
@ -38,8 +38,13 @@
|
|||||||
@open-new-tab-and-back="openNewTabAndBack"
|
@open-new-tab-and-back="openNewTabAndBack"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Mobile bottom browser bar — part of flex layout, doesn't overlay content -->
|
<!-- Mobile: gamepad for botfights, browser bar for everything else -->
|
||||||
<div class="md:hidden app-session-mobile-bar">
|
<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">
|
<button class="app-session-bar-btn" aria-label="Back" @click="iframeGoBack">
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<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" />
|
<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 NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
||||||
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
||||||
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
||||||
|
import MobileGamepad from './appSession/MobileGamepad.vue'
|
||||||
import {
|
import {
|
||||||
type DisplayMode, DISPLAY_MODE_KEY, NEW_TAB_APPS, IFRAME_BLOCKED_APPS,
|
type DisplayMode, DISPLAY_MODE_KEY, NEW_TAB_APPS, IFRAME_BLOCKED_APPS,
|
||||||
resolveAppUrl, resolveAppTitle,
|
resolveAppUrl, resolveAppTitle,
|
||||||
|
|||||||
216
neode-ui/src/views/appSession/MobileGamepad.vue
Normal file
216
neode-ui/src/views/appSession/MobileGamepad.vue
Normal 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>
|
||||||
@ -41,14 +41,14 @@ export const APP_PORTS: Record<string, number> = {
|
|||||||
'nostr-vpn': 8201,
|
'nostr-vpn': 8201,
|
||||||
'fips': 8202,
|
'fips': 8202,
|
||||||
'routstr': 8200,
|
'routstr': 8200,
|
||||||
'indeedhub': 7777,
|
'indeedhub': 7778,
|
||||||
'botfights': 9100,
|
'botfights': 9100,
|
||||||
'dwn': 3100,
|
'dwn': 3100,
|
||||||
'endurain': 8080,
|
'endurain': 8080,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apps that need nginx proxy for iframe embedding.
|
/** 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. */
|
* from the container's internal nginx so iframe works on all servers. */
|
||||||
export const PROXY_APPS: Record<string, string> = {}
|
export const PROXY_APPS: Record<string, string> = {}
|
||||||
|
|
||||||
@ -87,6 +87,7 @@ export const HTTPS_PROXY_PATHS: Record<string, string> = {
|
|||||||
'penpot': '/app/penpot/',
|
'penpot': '/app/penpot/',
|
||||||
'grafana': '/app/grafana/',
|
'grafana': '/app/grafana/',
|
||||||
'indeedhub': '/app/indeedhub/',
|
'indeedhub': '/app/indeedhub/',
|
||||||
|
'botfights': '/app/botfights/',
|
||||||
'routstr': '/app/routstr/',
|
'routstr': '/app/routstr/',
|
||||||
'nostr-vpn': '/app/nostr-vpn/',
|
'nostr-vpn': '/app/nostr-vpn/',
|
||||||
'fips': '/app/fips/',
|
'fips': '/app/fips/',
|
||||||
@ -143,7 +144,7 @@ export function resolveAppUrl(id: string, routeQueryPath?: string): string {
|
|||||||
const proxyPath = PROXY_APPS[id]
|
const proxyPath = PROXY_APPS[id]
|
||||||
if (proxyPath) return `${window.location.origin}${proxyPath}`
|
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') {
|
if (id === 'indeedhub') {
|
||||||
const port = APP_PORTS[id]
|
const port = APP_PORTS[id]
|
||||||
if (port) {
|
if (port) {
|
||||||
|
|||||||
@ -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: '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: '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: '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: '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: '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' },
|
{ 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' },
|
||||||
|
|||||||
@ -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.',
|
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',
|
icon: '/assets/img/app-icons/indeedhub.png',
|
||||||
author: 'Indeehub Team',
|
author: 'Indeehub Team',
|
||||||
dockerImage: 'localhost/indeedhub:latest',
|
dockerImage: 'git.tx1138.com/lfg2025/indeedhub:latest',
|
||||||
manifestUrl: undefined,
|
manifestUrl: undefined,
|
||||||
repoUrl: 'https://github.com/indeedhub/indeedhub'
|
repoUrl: 'https://github.com/indeedhub/indeedhub'
|
||||||
},
|
},
|
||||||
|
|||||||
@ -71,6 +71,9 @@ FIPS_UI_IMAGE="$ARCHY_REGISTRY/fips-ui:latest"
|
|||||||
# AI / Routing
|
# AI / Routing
|
||||||
ROUTSTR_IMAGE="$ARCHY_REGISTRY/routstr:v0.4.3"
|
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)
|
# IndeedHub stack (local builds use :local tag, not :latest)
|
||||||
MINIO_IMAGE="$ARCHY_REGISTRY/minio:RELEASE.2024-11-07T00-52-20Z"
|
MINIO_IMAGE="$ARCHY_REGISTRY/minio:RELEASE.2024-11-07T00-52-20Z"
|
||||||
INDEEDHUB_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:16.13-alpine"
|
INDEEDHUB_POSTGRES_IMAGE="$ARCHY_REGISTRY/postgres:16.13-alpine"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user