diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt
index 19e75422..07cac98e 100644
--- a/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt
+++ b/Android/app/src/main/java/com/archipelago/app/ui/components/NESController.kt
@@ -186,7 +186,7 @@ fun NESController(
}
}
- // A/B Buttons in inlay (same size as D-pad inlay, more right margin)
+ // A/B/C Buttons in inlay (same size as D-pad inlay, more right margin)
Inlay(c, Modifier.align(Alignment.CenterEnd).padding(end = 48.dp).size(140.dp)) {
Row(
Modifier.fillMaxSize(),
@@ -194,15 +194,20 @@ fun NESController(
verticalAlignment = Alignment.CenterVertically,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- Spacer(Modifier.height(10.dp))
- RoundBtn(c, 52.dp) { onKey("Escape") }
- Text("B", color = c.labelMuted, fontSize = 9.sp, fontWeight = FontWeight.Bold)
+ RoundBtn(c, 42.dp) { onKey("c") }
+ Text("C", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold)
}
- Spacer(Modifier.width(16.dp))
+ Spacer(Modifier.width(10.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
- RoundBtn(c, 52.dp) { onKey("Return") }
- Text("A", color = c.labelMuted, fontSize = 9.sp, fontWeight = FontWeight.Bold)
- Spacer(Modifier.height(10.dp))
+ Spacer(Modifier.height(8.dp))
+ RoundBtn(c, 42.dp) { onKey("b") }
+ Text("B", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold)
+ }
+ Spacer(Modifier.width(10.dp))
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ RoundBtn(c, 42.dp) { onKey("a") }
+ Text("A", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold)
+ Spacer(Modifier.height(8.dp))
}
}
}
diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt
index 662d160a..e825921a 100644
--- a/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt
+++ b/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -24,7 +25,9 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import com.archipelago.app.R
import com.archipelago.app.ui.theme.ControllerStyle
import com.archipelago.app.ui.theme.NES
@@ -113,16 +116,27 @@ fun NESPortraitController(
Spacer(Modifier.height(12.dp))
- // A/B Buttons
+ // A/B/C Buttons
Inlay(c, Modifier.fillMaxWidth()) {
Row(
- Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp),
+ Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
- RoundBtn(c, 52.dp) { onKey("Escape") }
- Spacer(Modifier.width(24.dp))
- RoundBtn(c, 52.dp) { onKey("Return") }
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ RoundBtn(c, 46.dp) { onKey("c") }
+ Text("C", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold)
+ }
+ Spacer(Modifier.width(16.dp))
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ RoundBtn(c, 46.dp) { onKey("b") }
+ Text("B", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold)
+ }
+ Spacer(Modifier.width(16.dp))
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ RoundBtn(c, 46.dp) { onKey("a") }
+ Text("A", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold)
+ }
}
}
diff --git a/app-catalog/README.md b/app-catalog/README.md
new file mode 100644
index 00000000..9e723be2
--- /dev/null
+++ b/app-catalog/README.md
@@ -0,0 +1,39 @@
+# Archipelago App Catalog
+
+Dynamic app catalog for the Archipelago marketplace. Nodes fetch this catalog to discover available apps.
+
+## How it works
+
+1. The Archipelago frontend fetches `catalog.json` from this repo
+2. Apps listed here appear in every node's app store automatically
+3. When a user installs an app, the backend pulls the Docker image and creates the container
+
+## Adding a new app
+
+Add an entry to `catalog.json`:
+
+```json
+{
+ "id": "my-app",
+ "title": "My App",
+ "version": "1.0.0",
+ "description": "What it does",
+ "icon": "/assets/img/app-icons/my-app.svg",
+ "author": "Author",
+ "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/my-app:1.0.0",
+ "repoUrl": "https://github.com/...",
+ "containerConfig": {
+ "ports": ["8080:8080"],
+ "volumes": ["/var/lib/archipelago/my-app:/data"],
+ "env": ["NODE_ENV=production"]
+ }
+}
+```
+
+For apps with hardcoded backend configs (Bitcoin, LND, etc.), `containerConfig` is optional.
+For new apps, include `containerConfig` so the backend knows how to create the container.
+
+## Categories
+
+money, commerce, data, home, nostr, networking, community, development, l484
diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json
new file mode 100644
index 00000000..8029a456
--- /dev/null
+++ b/app-catalog/catalog.json
@@ -0,0 +1,242 @@
+{
+ "version": 2,
+ "updated": "2026-04-12T00:00:00Z",
+ "registry": "git.tx1138.com/lfg2025",
+ "featured": {
+ "id": "indeedhub",
+ "banner": "/assets/img/featured/indeedhub-banner.jpg",
+ "headline": "Stream Sovereignty",
+ "description": "Bitcoin documentaries with Nostr identity.",
+ "tag": "NOSTR IDENTITY // YOUR NODE"
+ },
+ "apps": [
+ {
+ "id": "bitcoin-knots", "title": "Bitcoin Knots", "version": "28.1.0",
+ "description": "Run a full Bitcoin node. Validate and relay blocks and transactions.",
+ "icon": "/assets/img/app-icons/bitcoin-knots.webp",
+ "author": "Bitcoin Knots", "category": "money", "tier": "core",
+ "dockerImage": "git.tx1138.com/lfg2025/bitcoin-knots:latest",
+ "repoUrl": "https://github.com/bitcoinknots/bitcoin"
+ },
+ {
+ "id": "lnd", "title": "LND", "version": "0.18.4",
+ "description": "Lightning Network Daemon. Fast Bitcoin payments through Lightning.",
+ "icon": "/assets/img/app-icons/lnd.svg",
+ "author": "Lightning Labs", "category": "money", "tier": "core",
+ "dockerImage": "git.tx1138.com/lfg2025/lnd:v0.18.4-beta",
+ "repoUrl": "https://github.com/lightningnetwork/lnd",
+ "requires": ["bitcoin-knots"]
+ },
+ {
+ "id": "btcpay-server", "title": "BTCPay Server", "version": "1.13.7",
+ "description": "Self-hosted Bitcoin payment processor.",
+ "icon": "/assets/img/app-icons/btcpay-server.png",
+ "author": "BTCPay Server Foundation", "category": "commerce", "tier": "core",
+ "dockerImage": "git.tx1138.com/lfg2025/btcpayserver:1.13.7",
+ "repoUrl": "https://github.com/btcpayserver/btcpayserver",
+ "requires": ["bitcoin-knots"]
+ },
+ {
+ "id": "mempool", "title": "Mempool Explorer", "version": "3.0.0",
+ "description": "Self-hosted Bitcoin blockchain and mempool visualizer.",
+ "icon": "/assets/img/app-icons/mempool.webp",
+ "author": "Mempool", "category": "money", "tier": "core",
+ "dockerImage": "git.tx1138.com/lfg2025/mempool-frontend:v3.0.0",
+ "repoUrl": "https://github.com/mempool/mempool",
+ "requires": ["bitcoin-knots", "electrumx"]
+ },
+ {
+ "id": "electrumx", "title": "ElectrumX", "version": "1.18.0",
+ "description": "Electrum protocol server. Index the blockchain for fast wallet lookups.",
+ "icon": "/assets/img/app-icons/electrumx.webp",
+ "author": "Luke Childs", "category": "money", "tier": "core",
+ "dockerImage": "git.tx1138.com/lfg2025/electrumx:v1.18.0",
+ "repoUrl": "https://github.com/spesmilo/electrumx",
+ "requires": ["bitcoin-knots"]
+ },
+ {
+ "id": "indeedhub", "title": "IndeeHub", "version": "1.0.0",
+ "description": "Bitcoin documentary streaming with Nostr identity.",
+ "icon": "/assets/img/app-icons/indeedhub.png",
+ "author": "IndeeHub", "category": "community",
+ "dockerImage": "git.tx1138.com/lfg2025/indeedhub:1.0.0",
+ "repoUrl": "https://github.com/indeedhub/indeedhub"
+ },
+ {
+ "id": "botfights", "title": "BotFights", "version": "1.1.0",
+ "description": "Bot arena + 2-player arcade fighter with controller support and Adventure Mode.",
+ "icon": "/assets/img/app-icons/botfights.svg",
+ "author": "BotFights", "category": "community",
+ "dockerImage": "git.tx1138.com/lfg2025/botfights:1.1.0",
+ "repoUrl": "https://botfights.net"
+ },
+ {
+ "id": "gitea", "title": "Gitea", "version": "1.23",
+ "description": "Self-hosted Git service with container registry, CI/CD, issue tracking.",
+ "icon": "/assets/img/app-icons/gitea.svg",
+ "author": "Gitea", "category": "development",
+ "dockerImage": "docker.io/gitea/gitea:1.23",
+ "repoUrl": "https://gitea.com"
+ },
+ {
+ "id": "filebrowser", "title": "File Browser", "version": "2.27.0",
+ "description": "Web-based file manager.",
+ "icon": "/assets/img/app-icons/file-browser.webp",
+ "author": "File Browser", "category": "data", "tier": "core",
+ "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0",
+ "repoUrl": "https://github.com/filebrowser/filebrowser"
+ },
+ {
+ "id": "vaultwarden", "title": "Vaultwarden", "version": "1.30.0",
+ "description": "Self-hosted password vault with zero-knowledge encryption.",
+ "icon": "/assets/img/app-icons/vaultwarden.webp",
+ "author": "Vaultwarden", "category": "data", "tier": "recommended",
+ "dockerImage": "git.tx1138.com/lfg2025/vaultwarden:1.30.0-alpine",
+ "repoUrl": "https://github.com/dani-garcia/vaultwarden"
+ },
+ {
+ "id": "searxng", "title": "SearXNG", "version": "2024.1.0",
+ "description": "Privacy-respecting metasearch engine.",
+ "icon": "/assets/img/app-icons/searxng.png",
+ "author": "SearXNG", "category": "data", "tier": "recommended",
+ "dockerImage": "git.tx1138.com/lfg2025/searxng:latest",
+ "repoUrl": "https://github.com/searxng/searxng"
+ },
+ {
+ "id": "nostr-rs-relay", "title": "Nostr Relay", "version": "0.9.0",
+ "description": "Your own Nostr relay. Store events locally, relay for friends.",
+ "icon": "/assets/img/app-icons/nostr-rs-relay.svg",
+ "author": "scsiblade", "category": "nostr",
+ "dockerImage": "git.tx1138.com/lfg2025/nostr-rs-relay:0.9.0",
+ "repoUrl": "https://sr.ht/~gheartsfield/nostr-rs-relay/"
+ },
+ {
+ "id": "fedimint", "title": "Fedimint", "version": "0.10.0",
+ "description": "Federated Bitcoin mint with privacy through federated guardians.",
+ "icon": "/assets/img/app-icons/fedimint.png",
+ "author": "Fedimint", "category": "money",
+ "dockerImage": "git.tx1138.com/lfg2025/fedimintd:v0.10.0",
+ "repoUrl": "https://github.com/fedimint/fedimint"
+ },
+ {
+ "id": "ollama", "title": "Ollama", "version": "0.5.4",
+ "description": "Run AI models locally. Private and on your hardware.",
+ "icon": "/assets/img/app-icons/ollama.png",
+ "author": "Ollama", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/ollama:latest",
+ "repoUrl": "https://github.com/ollama/ollama"
+ },
+ {
+ "id": "nextcloud", "title": "Nextcloud", "version": "28",
+ "description": "Your own private cloud. File sync, calendars, contacts.",
+ "icon": "/assets/img/app-icons/nextcloud.webp",
+ "author": "Nextcloud", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/nextcloud:28",
+ "repoUrl": "https://github.com/nextcloud/server"
+ },
+ {
+ "id": "jellyfin", "title": "Jellyfin", "version": "10.8.13",
+ "description": "Free media server. Stream movies, music, and photos.",
+ "icon": "/assets/img/app-icons/jellyfin.webp",
+ "author": "Jellyfin", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/jellyfin:10.8.13",
+ "repoUrl": "https://github.com/jellyfin/jellyfin"
+ },
+ {
+ "id": "immich", "title": "Immich", "version": "1.90.0",
+ "description": "High-performance photo and video backup with ML.",
+ "icon": "/assets/img/app-icons/immich.png",
+ "author": "Immich", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/immich-server:release",
+ "repoUrl": "https://github.com/immich-app/immich"
+ },
+ {
+ "id": "homeassistant", "title": "Home Assistant", "version": "2024.1",
+ "description": "Open-source home automation.",
+ "icon": "/assets/img/app-icons/homeassistant.png",
+ "author": "Home Assistant", "category": "home",
+ "dockerImage": "git.tx1138.com/lfg2025/home-assistant:2024.1",
+ "repoUrl": "https://github.com/home-assistant/core"
+ },
+ {
+ "id": "grafana", "title": "Grafana", "version": "10.2.0",
+ "description": "Analytics and monitoring dashboards.",
+ "icon": "/assets/img/app-icons/grafana.png",
+ "author": "Grafana Labs", "category": "data", "tier": "recommended",
+ "dockerImage": "git.tx1138.com/lfg2025/grafana:10.2.0",
+ "repoUrl": "https://github.com/grafana/grafana"
+ },
+ {
+ "id": "tailscale", "title": "Tailscale", "version": "1.78.0",
+ "description": "Zero-config VPN with WireGuard mesh networking.",
+ "icon": "/assets/img/app-icons/tailscale.webp",
+ "author": "Tailscale", "category": "networking", "tier": "recommended",
+ "dockerImage": "git.tx1138.com/lfg2025/tailscale:stable",
+ "repoUrl": "https://github.com/tailscale/tailscale"
+ },
+ {
+ "id": "uptime-kuma", "title": "Uptime Kuma", "version": "1.23.0",
+ "description": "Self-hosted uptime monitoring.",
+ "icon": "/assets/img/app-icons/uptime-kuma.webp",
+ "author": "Uptime Kuma", "category": "data", "tier": "recommended",
+ "dockerImage": "git.tx1138.com/lfg2025/uptime-kuma:1",
+ "repoUrl": "https://github.com/louislam/uptime-kuma"
+ },
+ {
+ "id": "nostr-vpn", "title": "Nostr VPN", "version": "0.3.7",
+ "description": "Tailscale-style mesh VPN with Nostr control plane.",
+ "icon": "/assets/img/app-icons/nostr-vpn.svg",
+ "author": "Martti Malmi", "category": "networking",
+ "dockerImage": "git.tx1138.com/lfg2025/nostr-vpn:v0.3.7",
+ "repoUrl": "https://github.com/mmalmi/nostr-vpn"
+ },
+ {
+ "id": "fips", "title": "FIPS", "version": "0.1.0",
+ "description": "Free Internetworking Peering System. Encrypted mesh network.",
+ "icon": "/assets/img/app-icons/fips.svg",
+ "author": "Jim Corgan", "category": "networking",
+ "dockerImage": "git.tx1138.com/lfg2025/fips:v0.1.0",
+ "repoUrl": "https://github.com/jmcorgan/fips"
+ },
+ {
+ "id": "routstr", "title": "Routstr", "version": "0.4.3",
+ "description": "Decentralized AI inference proxy with Cashu ecash.",
+ "icon": "/assets/img/app-icons/routstr.svg",
+ "author": "Routstr", "category": "community",
+ "dockerImage": "git.tx1138.com/lfg2025/routstr:v0.4.3",
+ "repoUrl": "https://github.com/routstr/routstr-core"
+ },
+ {
+ "id": "dwn", "title": "Decentralized Web Node", "version": "0.4.0",
+ "description": "Own your data with DID-based access control.",
+ "icon": "/assets/img/app-icons/dwn.svg",
+ "author": "TBD", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/dwn-server:main",
+ "repoUrl": "https://github.com/TBD54566975/dwn-server"
+ },
+ {
+ "id": "endurain", "title": "Endurain", "version": "0.8.0",
+ "description": "Self-hosted fitness tracking. Strava alternative.",
+ "icon": "/assets/img/app-icons/endurain.png",
+ "author": "Endurain", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/endurain:0.8.0",
+ "repoUrl": "https://github.com/joaovitoriasilva/endurain"
+ },
+ {
+ "id": "penpot", "title": "Penpot", "version": "2.4",
+ "description": "Open-source design platform. Self-hosted Figma alternative.",
+ "icon": "/assets/img/app-icons/penpot.webp",
+ "author": "Penpot", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/penpot-frontend:2.4",
+ "repoUrl": "https://github.com/penpot/penpot"
+ },
+ {
+ "id": "photoprism", "title": "PhotoPrism", "version": "240915",
+ "description": "AI-powered photo management with facial recognition.",
+ "icon": "/assets/img/app-icons/photoprism.svg",
+ "author": "PhotoPrism", "category": "data",
+ "dockerImage": "git.tx1138.com/lfg2025/photoprism:240915",
+ "repoUrl": "https://github.com/photoprism/photoprism"
+ }
+ ]
+}
diff --git a/apps/gitea/manifest.yml b/apps/gitea/manifest.yml
index 4af96688..8f116c81 100644
--- a/apps/gitea/manifest.yml
+++ b/apps/gitea/manifest.yml
@@ -5,6 +5,7 @@ description: Self-hosted Git service with built-in container registry, CI/CD, an
category: development
icon: git-branch
port: 3000
+internal_port: 3001
ssh_port: 2222
image: docker.io/gitea/gitea:1.23
tier: optional
@@ -28,6 +29,16 @@ environment:
GITEA__repository__ENABLE_PUSH_CREATE_USER: "true"
GITEA__repository__ENABLE_PUSH_CREATE_ORG: "true"
+# Gitea hardcodes X-Frame-Options: SAMEORIGIN which blocks iframe embedding.
+# Container binds to internal_port (3001), nginx proxies public port (3000)
+# stripping the X-Frame-Options header so the app works in Archipelago's iframe.
+nginx_proxy:
+ listen: 3000
+ proxy_pass: "http://127.0.0.1:3001"
+ extra_headers:
+ - "proxy_hide_header X-Frame-Options"
+ - "proxy_hide_header Content-Security-Policy"
+
health_check:
endpoint: /
interval: 120
diff --git a/core/archipelago/src/api/handler/remote_input.rs b/core/archipelago/src/api/handler/remote_input.rs
index 123cb24f..56525169 100644
--- a/core/archipelago/src/api/handler/remote_input.rs
+++ b/core/archipelago/src/api/handler/remote_input.rs
@@ -4,7 +4,6 @@ use hyper::{Request, Response};
use hyper_ws_listener::WsStream;
use serde::Deserialize;
use std::time::Instant;
-use tokio::process::Command;
use tokio::sync::broadcast;
use tokio_tungstenite::tungstenite::Message;
use tracing::{debug, info, warn};
@@ -72,21 +71,9 @@ enum InputCommand {
Ping,
}
-async fn xdotool(args: &[&str]) -> Result<()> {
- let output = Command::new("xdotool")
- .env("DISPLAY", ":0")
- .args(args)
- .output()
- .await
- .context("xdotool execution failed")?;
-
- if !output.status.success() {
- let stderr = String::from_utf8_lossy(&output.stderr);
- debug!("xdotool error: {}", stderr);
- }
- Ok(())
-}
-
+/// Validate and acknowledge input — relay-only, no xdotool.
+/// All input is forwarded to browser clients via the broadcast channel;
+/// the browser's remote-relay.ts dispatches DOM events from there.
async fn handle_input(msg: &str) -> Result