fix: ISO install - fallback registry, filebrowser noauth, registries

1. registries.conf includes docker.io search + fallback 23.182.128.160
2. First-boot pull_with_fallback() tries primary then fallback registry
3. FileBrowser created with noauth config on persistent volume
4. Backend dynamic registries.json pre-created in ISO
5. Filebrowser password secret created for token flow

Fixes: apps stuck at 0% download, filebrowser not working, dynamic
catalog not loading on fresh installs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-04-12 09:06:12 -04:00
parent ff5ef2951f
commit bf7bc7f104
6 changed files with 144 additions and 78 deletions

View File

@@ -11,8 +11,8 @@ android {
applicationId = "com.archipelago.app" applicationId = "com.archipelago.app"
minSdk = 26 minSdk = 26
targetSdk = 35 targetSdk = 35
versionCode = 4 versionCode = 6
versionName = "0.4.0" versionName = "0.4.2"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

View File

@@ -186,28 +186,20 @@ fun NESController(
} }
} }
// A/B/C Buttons in inlay (same size as D-pad inlay, more right margin) // A/B/C Buttons in inlay — triangle: C top, B+A bottom
Inlay(c, Modifier.align(Alignment.CenterEnd).padding(end = 48.dp).size(140.dp)) { Inlay(c, Modifier.align(Alignment.CenterEnd).padding(end = 48.dp).size(140.dp)) {
Row( Column(
Modifier.fillMaxSize(), Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically, verticalArrangement = Arrangement.Center,
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { // C on top (white)
RoundBtn(c, 42.dp) { onKey("c") } ColorBtn(Color(0xFF888888), Color(0xFFAAAAAA), 44.dp) { onKey("c") }
Text("C", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold) Spacer(Modifier.height(6.dp))
} // B + A on bottom row
Spacer(Modifier.width(10.dp)) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { ColorBtn(Color(0xFF3B82F6), Color(0xFF60A5FA), 44.dp) { onKey("b") }
Spacer(Modifier.height(8.dp)) ColorBtn(Color(0xFFEA580C), Color(0xFFFB923C), 44.dp) { onKey("a") }
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))
} }
} }
} }
@@ -361,6 +353,28 @@ fun RoundBtn(c: NESPalette, sz: Dp = 52.dp, onClick: () -> Unit) {
} }
} }
/** Colored round button — custom color instead of palette */
@Composable
fun ColorBtn(color: Color, pressColor: Color, sz: Dp = 48.dp, onClick: () -> Unit) {
var p by remember { mutableStateOf(false) }
Box(
Modifier
.size(sz)
.shadow(if (p) 1.dp else 4.dp, CircleShape)
.clip(CircleShape)
.background(Brush.verticalGradient(
if (p) listOf(pressColor, color.copy(alpha = 0.85f))
else listOf(color, color.copy(alpha = 0.8f))
))
.pointerInput(Unit) { detectTapGestures(onPress = { p = true; onClick(); tryAwaitRelease(); p = false }) },
contentAlignment = Alignment.Center,
) {
if (!p) Box(Modifier.fillMaxSize().clip(CircleShape).background(
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.18f), Color.Transparent))
))
}
}
/** START/SELECT capsule */ /** START/SELECT capsule */
@Composable @Composable
fun CapsuleBtn(label: String, c: NESPalette, w: Dp = 64.dp, h: Dp = 28.dp, onClick: () -> Unit) { fun CapsuleBtn(label: String, c: NESPalette, w: Dp = 64.dp, h: Dp = 28.dp, onClick: () -> Unit) {

View File

@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -25,9 +24,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.archipelago.app.R import com.archipelago.app.R
import com.archipelago.app.ui.theme.ControllerStyle import com.archipelago.app.ui.theme.ControllerStyle
import com.archipelago.app.ui.theme.NES import com.archipelago.app.ui.theme.NES
@@ -116,26 +113,17 @@ fun NESPortraitController(
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(12.dp))
// A/B/C Buttons // A/B/C Buttons — triangle: C top, B+A bottom
Inlay(c, Modifier.fillMaxWidth()) { Inlay(c, Modifier.fillMaxWidth()) {
Row( Column(
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 10.dp), Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) { ) {
Column(horizontalAlignment = Alignment.CenterHorizontally) { ColorBtn(Color(0xFF888888), Color(0xFFAAAAAA), 46.dp) { onKey("c") }
RoundBtn(c, 46.dp) { onKey("c") } Spacer(Modifier.height(6.dp))
Text("C", color = c.labelMuted, fontSize = 8.sp, fontWeight = FontWeight.Bold) Row(horizontalArrangement = Arrangement.spacedBy(14.dp)) {
} ColorBtn(Color(0xFF3B82F6), Color(0xFF60A5FA), 46.dp) { onKey("b") }
Spacer(Modifier.width(16.dp)) ColorBtn(Color(0xFFEA580C), Color(0xFFFB923C), 46.dp) { onKey("a") }
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)
} }
} }
} }

View File

@@ -2113,14 +2113,32 @@ STORAGECONF
# Symlink for backward compat (some tools look in ~/.local/share/containers) # Symlink for backward compat (some tools look in ~/.local/share/containers)
ln -sf /var/lib/archipelago/containers/storage /mnt/target/home/archipelago/.local/share/containers/storage 2>/dev/null || true ln -sf /var/lib/archipelago/containers/storage /mnt/target/home/archipelago/.local/share/containers/storage 2>/dev/null || true
# Configure Archipelago app registry (HTTP, insecure) # Configure Archipelago app registries (primary + fallback)
cat > /mnt/target/home/archipelago/.config/containers/registries.conf <<'REGCONF' cat > /mnt/target/home/archipelago/.config/containers/registries.conf <<'REGCONF'
unqualified-search-registries = ["docker.io"]
[[registry]] [[registry]]
location = "git.tx1138.com" location = "git.tx1138.com"
insecure = true insecure = true
[[registry]]
location = "23.182.128.160:3000"
insecure = true
REGCONF REGCONF
chown -R 1000:1000 /mnt/target/home/archipelago/.config chown -R 1000:1000 /mnt/target/home/archipelago/.config
# Pre-create dynamic registry config for the backend (fallback registries)
mkdir -p /mnt/target/var/lib/archipelago/config
cat > /mnt/target/var/lib/archipelago/config/registries.json <<'DYNREG'
{
"registries": [
{"url": "git.tx1138.com/lfg2025", "name": "Archipelago Primary", "tls_verify": true, "enabled": true, "priority": 0},
{"url": "23.182.128.160:3000/lfg2025", "name": "Archipelago Fallback", "tls_verify": false, "enabled": true, "priority": 10}
]
}
DYNREG
chown -R 1000:1000 /mnt/target/var/lib/archipelago/config
# Configure podman to use netavark backend (enables container DNS on archy-net). # Configure podman to use netavark backend (enables container DNS on archy-net).
# netavark + aardvark-dns binaries come from the rootfs (Debian 13 apt packages). # netavark + aardvark-dns binaries come from the rootfs (Debian 13 apt packages).
if [ -f /mnt/target/usr/lib/podman/netavark ]; then if [ -f /mnt/target/usr/lib/podman/netavark ]; then

View File

@@ -76,7 +76,7 @@
</div> </div>
</div> </div>
<!-- Action buttons (right side) --> <!-- Action buttons (right side) triangle layout -->
<div class="gamepad-actions"> <div class="gamepad-actions">
<button <button
class="action-btn action-c" class="action-btn action-c"
@@ -84,21 +84,23 @@
@touchend.prevent="up('c')" @touchend.prevent="up('c')"
@touchcancel.prevent="up('c')" @touchcancel.prevent="up('c')"
aria-label="Special" aria-label="Special"
>C</button> ></button>
<button <div class="action-row">
class="action-btn action-b" <button
@touchstart.prevent="down('b')" class="action-btn action-b"
@touchend.prevent="up('b')" @touchstart.prevent="down('b')"
@touchcancel.prevent="up('b')" @touchend.prevent="up('b')"
aria-label="Kick" @touchcancel.prevent="up('b')"
>B</button> aria-label="Kick"
<button ></button>
class="action-btn action-a" <button
@touchstart.prevent="down('a')" class="action-btn action-a"
@touchend.prevent="up('a')" @touchstart.prevent="down('a')"
@touchcancel.prevent="up('a')" @touchend.prevent="up('a')"
aria-label="Punch" @touchcancel.prevent="up('a')"
>A</button> aria-label="Punch"
></button>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -233,20 +235,24 @@ function tap(key: string) { send(key, 'down'); setTimeout(() => send(key, 'up'),
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
/* ── Action buttons (A / B / C) ── */ /* ── Action buttons — triangle layout ── */
.gamepad-actions { .gamepad-actions {
display: flex; display: flex;
gap: 10px; flex-direction: column;
align-items: center; align-items: center;
flex-shrink: 0; flex-shrink: 0;
gap: 6px;
}
.action-row {
display: flex;
gap: 10px;
} }
.action-btn { .action-btn {
width: 54px; width: 50px;
height: 54px; height: 50px;
border-radius: 50%; border-radius: 50%;
font-size: 18px;
font-weight: 800;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -276,11 +282,11 @@ function tap(key: string) { send(key, 'down'); setTimeout(() => send(key, 'up'),
} }
.action-c { .action-c {
background: rgba(74, 222, 128, 0.2); background: rgba(255, 255, 255, 0.12);
border-color: rgba(74, 222, 128, 0.5); border-color: rgba(255, 255, 255, 0.4);
color: #4ade80; color: #ffffff;
} }
.action-c:active { .action-c:active {
background: rgba(74, 222, 128, 0.45); background: rgba(255, 255, 255, 0.3);
} }
</style> </style>

View File

@@ -78,27 +78,67 @@ if [ -f "$UNBUNDLED_MARKER" ]; then
# Ensure archy-net exists # Ensure archy-net exists
$DOCKER network create archy-net 2>/dev/null || true $DOCKER network create archy-net 2>/dev/null || true
# Create FileBrowser only # Helper: pull image with fallback registry
pull_with_fallback() {
local img="$1"
log " Pulling $img..."
if $DOCKER pull "$img" 2>>"$LOG"; then
return 0
fi
# Try fallback registry
local fallback_img
fallback_img=$(echo "$img" | sed "s|${ARCHY_REGISTRY}|${ARCHY_REGISTRY_FALLBACK}|")
if [ "$fallback_img" != "$img" ] && [ -n "$ARCHY_REGISTRY_FALLBACK" ]; then
log " Primary failed, trying fallback: $fallback_img"
if $DOCKER pull "$fallback_img" --tls-verify=false 2>>"$LOG"; then
$DOCKER tag "$fallback_img" "$img" 2>/dev/null
return 0
fi
fi
# Try docker.io as last resort for common images
local short_name
short_name=$(echo "$img" | sed 's|.*/||')
local dockerhub="docker.io/library/$short_name"
log " Fallback failed, trying docker.io: $dockerhub"
$DOCKER pull "$dockerhub" 2>>"$LOG" && $DOCKER tag "$dockerhub" "$img" 2>/dev/null && return 0
return 1
}
# Create FileBrowser (noauth — behind Archipelago login)
if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
log "Creating FileBrowser..." log "Creating FileBrowser (noauth)..."
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-data mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-data
mkdir -p /var/lib/archipelago/data/cloud/{Documents,Photos,Music,Videos,Downloads} mkdir -p /var/lib/archipelago/filebrowser/{Documents,Photos,Music,Videos,Downloads}
sudo chown -R 100000:100000 /var/lib/archipelago/filebrowser chown -R 1000:1000 /var/lib/archipelago/filebrowser
sudo chown -R 100000:100000 /var/lib/archipelago/filebrowser-data chown -R 1000:1000 /var/lib/archipelago/filebrowser-data
sudo chown -R 100000:100000 /var/lib/archipelago/data # Write config with database on persistent volume
cat > /var/lib/archipelago/filebrowser-data/.filebrowser.json <<'FBEOF'
{"port":80,"baseURL":"","address":"0.0.0.0","database":"/data/filebrowser.db","root":"/srv","log":"stdout"}
FBEOF
chown 1000:1000 /var/lib/archipelago/filebrowser-data/.filebrowser.json
pull_with_fallback "${FILEBROWSER_IMAGE}"
$DOCKER run -d --name filebrowser --restart unless-stopped \ $DOCKER run -d --name filebrowser --restart unless-stopped \
--network archy-net \
--cap-drop=ALL --cap-add=DAC_OVERRIDE --cap-add=NET_BIND_SERVICE \ --cap-drop=ALL --cap-add=DAC_OVERRIDE --cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges:true \ --security-opt=no-new-privileges:true \
--health-cmd='curl -sf http://localhost:80/ || exit 1' \ --health-cmd='wget -q --spider http://localhost:80/health || exit 1' \
--health-interval=30s --health-timeout=5s --health-retries=3 \ --health-interval=30s --health-timeout=5s --health-retries=3 \
--memory=256m \ --memory=256m \
-p 8083:80 \ -p 8083:80 \
-v /var/lib/archipelago/filebrowser:/srv \ -v /var/lib/archipelago/filebrowser:/srv \
-v /var/lib/archipelago/filebrowser-data:/data \ -v /var/lib/archipelago/filebrowser-data:/data \
-v /var/lib/archipelago/data/cloud:/srv/cloud \
${FILEBROWSER_IMAGE} \ ${FILEBROWSER_IMAGE} \
--database=/data/database.db --root=/srv --address=0.0.0.0 --port=80 2>>"$LOG" && \ --config /data/.filebrowser.json 2>>"$LOG" && \
log " FileBrowser created" || log " WARNING: FileBrowser creation failed" log " FileBrowser created" || log " WARNING: FileBrowser creation failed"
# Set noauth after first start
sleep 3
$DOCKER exec filebrowser /filebrowser config set --auth.method=noauth --database /data/filebrowser.db 2>>"$LOG" || true
$DOCKER exec filebrowser /filebrowser users add admin admin --perm.admin --database /data/filebrowser.db 2>>"$LOG" || true
$DOCKER restart filebrowser 2>>"$LOG" || true
# Create filebrowser password for backend token flow
mkdir -p /var/lib/archipelago/secrets/filebrowser
echo -n "admin" > /var/lib/archipelago/secrets/filebrowser/password
chown -R 1000:1000 /var/lib/archipelago/secrets
fi fi
log "Unbundled first-boot complete" log "Unbundled first-boot complete"