fix(demo): never leak the release-server IP in the public demo
Some checks failed
Demo images / Build & push demo images (push) Failing after 5m0s

- Companion QR overlay: demo builds encode the demo's own origin
  (/packages/archipelago-companion.apk ships in the web image) instead of
  the vps2 raw URL.
- Dockerfile.web/.backend: build-time scrub replaces every remaining
  occurrence of the release-server address with registry.demo.internal and
  fails the build if any survives.
- Mock backend update-mirror list, sideload help text, and changelog prose
  no longer name the server address.
- Copy demo-images workflow into .gitea/workflows/ — Gitea ignores
  .github/workflows when .gitea/workflows exists, so the CI never ran.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago 2026-07-14 11:38:51 -04:00
parent ad3dae9983
commit 79564486d3
7 changed files with 106 additions and 5 deletions

View File

@ -0,0 +1,68 @@
name: Demo images
# Builds and pushes the public-demo images on every change to the UI / mock
# backend, so the separated `archy-demo` Portainer stack auto-tracks the real
# code (see demo-deploy/ and docs/demo-deployment-design.md).
#
# Required repo configuration:
# vars.DEMO_REGISTRY e.g. 146.59.87.168:3000/lfg2025
# vars.DEMO_REGISTRY_HOST registry host for docker login (no org suffix)
# secrets.DEMO_REGISTRY_USER
# secrets.DEMO_REGISTRY_TOKEN
# Optional:
# secrets.PORTAINER_WEBHOOK redeploy hook called after a successful push
on:
push:
branches: [main]
paths:
- 'neode-ui/**'
- 'docker-compose.demo.yml'
- '.github/workflows/demo-images.yml'
workflow_dispatch:
jobs:
build:
name: Build & push demo images
runs-on: ubuntu-latest
# Skip cleanly on forks / before registry config is set.
if: ${{ vars.DEMO_REGISTRY != '' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to registry
uses: docker/login-action@v3
with:
registry: ${{ vars.DEMO_REGISTRY_HOST || vars.DEMO_REGISTRY }}
username: ${{ secrets.DEMO_REGISTRY_USER }}
password: ${{ secrets.DEMO_REGISTRY_TOKEN }}
- name: Build & push backend
uses: docker/build-push-action@v6
with:
context: .
file: neode-ui/Dockerfile.backend
push: true
tags: |
${{ vars.DEMO_REGISTRY }}/archy-demo-backend:demo
${{ vars.DEMO_REGISTRY }}/archy-demo-backend:${{ github.sha }}
- name: Build & push web
uses: docker/build-push-action@v6
with:
context: .
file: neode-ui/Dockerfile.web
push: true
build-args: |
VITE_DEMO=1
tags: |
${{ vars.DEMO_REGISTRY }}/archy-demo-web:demo
${{ vars.DEMO_REGISTRY }}/archy-demo-web:${{ github.sha }}
- name: Trigger Portainer redeploy
if: ${{ success() && secrets.PORTAINER_WEBHOOK != '' }}
run: curl -fsS -X POST "${{ secrets.PORTAINER_WEBHOOK }}"

View File

@ -24,6 +24,19 @@ COPY docker/fedimint-ui /docker/fedimint-ui
COPY demo/files /demo/files COPY demo/files /demo/files
COPY demo/content /demo/content COPY demo/content /demo/content
# This image only ever serves the public demo — scrub the private release/registry
# server address from everything it serves (mock data, catalog.json, demo assets)
# and fail the build if any occurrence survives.
RUN find /app /docker /demo -type f \( -name '*.js' -o -name '*.mjs' -o -name '*.cjs' \
-o -name '*.css' -o -name '*.html' -o -name '*.json' -o -name '*.md' -o -name '*.txt' \
-o -name '*.yml' -o -name '*.yaml' \) -not -path '*/node_modules/*' \
-exec sed -i \
-e 's#146\.59\.87\.168:3000/lfg2025#registry.demo.internal/archy#g' \
-e 's#146\.59\.87\.168:3000#registry.demo.internal#g' \
-e 's#146\.59\.87\.168#registry.demo.internal#g' {} + && \
if grep -rq '146\.59\.87\.168' /app/mock-backend.js /app/public /docker /demo; then \
echo 'LEAK: release-server IP still in demo image'; exit 1; fi
# Expose port # Expose port
EXPOSE 5959 EXPOSE 5959

View File

@ -29,6 +29,20 @@ ENV VITE_DEMO=$VITE_DEMO
# Use npm script which handles build better # Use npm script which handles build better
RUN npm run build:docker || (echo "Build failed! Listing files:" && ls -la && echo "Checking vite config:" && cat vite.config.ts && exit 1) RUN npm run build:docker || (echo "Build failed! Listing files:" && ls -la && echo "Checking vite config:" && cat vite.config.ts && exit 1)
# Demo builds must not leak the private release/registry server address anywhere
# in the served bundle (JS constants, catalog.json, prose). Replace every
# occurrence with a non-routable placeholder, then fail the build if any slipped
# through. (The companion-QR URL is handled in source — it needs a URL that works.)
RUN if [ "$VITE_DEMO" = "1" ] || [ "$VITE_DEMO" = "true" ]; then \
find dist -type f \( -name '*.js' -o -name '*.mjs' -o -name '*.css' -o -name '*.html' \
-o -name '*.json' -o -name '*.map' -o -name '*.txt' -o -name '*.webmanifest' \) \
-exec sed -i \
-e 's#146\.59\.87\.168:3000/lfg2025#registry.demo.internal/archy#g' \
-e 's#146\.59\.87\.168:3000#registry.demo.internal#g' \
-e 's#146\.59\.87\.168#registry.demo.internal#g' {} + && \
if grep -rq '146\.59\.87\.168' dist; then echo 'LEAK: release-server IP still in dist'; exit 1; fi; \
fi
# Production stage # Production stage
FROM nginx:alpine FROM nginx:alpine

View File

@ -3561,7 +3561,8 @@ app.post('/rpc/v1', (req, res) => {
case 'update.list-mirrors': { case 'update.list-mirrors': {
globalThis.__mockMirrors ||= [ globalThis.__mockMirrors ||= [
{ url: 'http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/releases/manifest.json', label: 'Origin (vps2)' }, // Neutral placeholder — the public demo must not reveal the real release-server address.
{ url: 'https://updates.archipelago.demo/releases/manifest.json', label: 'Origin' },
] ]
return res.json({ result: { mirrors: globalThis.__mockMirrors } }) return res.json({ result: { mirrors: globalThis.__mockMirrors } })
} }

View File

@ -77,12 +77,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import * as QRCode from 'qrcode' import * as QRCode from 'qrcode'
import { IS_DEMO } from '@/composables/useDemoIntro'
const STORAGE_KEY = 'neode_companion_intro_seen' const STORAGE_KEY = 'neode_companion_intro_seen'
// Absolute URL so the QR works when scanned by a phone (a relative path has no // Absolute URL so the QR works when scanned by a phone (a relative path has no
// host to resolve). Points at the companion APK hosted on the 146 release server // host to resolve). Points at the companion APK hosted on the 146 release server
// (publicly reachable) rather than the local node's /packages copy. // (publicly reachable) rather than the local node's /packages copy.
const DEFAULT_DOWNLOAD_URL = 'http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/neode-ui/public/packages/archipelago-companion.apk' // The demo serves the APK from its own public origin instead, so the QR never
// exposes the release-server address.
const DEFAULT_DOWNLOAD_URL = IS_DEMO
? `${window.location.origin}/packages/archipelago-companion.apk`
: 'http://146.59.87.168:3000/lfg2025/archy/raw/branch/main/neode-ui/public/packages/archipelago-companion.apk'
const visible = ref(false) const visible = ref(false)
const qrDataUrl = ref('') const qrDataUrl = ref('')

View File

@ -348,7 +348,7 @@
<div class="mt-5 rounded-xl border border-white/10 bg-white/[0.04] p-4 text-sm text-white/65"> <div class="mt-5 rounded-xl border border-white/10 bg-white/[0.04] p-4 text-sm text-white/65">
<p class="font-medium text-white/80 mb-2">Easy sources</p> <p class="font-medium text-white/80 mb-2">Easy sources</p>
<p>Use images from Docker Hub, GHCR, the VPS2 Gitea registry (146.59.87.168:3000), or localhost. Good first candidates: Excalidraw, Stirling PDF, FreshRSS, Wallabag, HedgeDoc, CyberChef, Mealie, or PairDrop.</p> <p>Use images from Docker Hub, GHCR, the Archipelago app registry, or localhost. Good first candidates: Excalidraw, Stirling PDF, FreshRSS, Wallabag, HedgeDoc, CyberChef, Mealie, or PairDrop.</p>
</div> </div>
<div class="mt-5 flex gap-3"> <div class="mt-5 flex gap-3">

View File

@ -1068,7 +1068,7 @@ init()
<p>Installing, updating, and removing apps no longer freezes the UI. The backend now spawns the actual work in the background and returns immediately, so the progress bar starts moving right away instead of the whole page locking up for 30+ seconds while podman pulls an image.</p> <p>Installing, updating, and removing apps no longer freezes the UI. The backend now spawns the actual work in the background and returns immediately, so the progress bar starts moving right away instead of the whole page locking up for 30+ seconds while podman pulls an image.</p>
<p>Install progress bar actually reflects reality now. It previously stayed at 0% until the very end because podman doesn't emit parseable progress when run without a TTY. Replaced byte-counting with seven clearly-labelled phases Preparing, Pulling image, Creating container, Starting, Waiting for health, Finalizing, Done each mapped to a fixed percentage so the bar only moves forward.</p> <p>Install progress bar actually reflects reality now. It previously stayed at 0% until the very end because podman doesn't emit parseable progress when run without a TTY. Replaced byte-counting with seven clearly-labelled phases Preparing, Pulling image, Creating container, Starting, Waiting for health, Finalizing, Done each mapped to a fixed percentage so the bar only moves forward.</p>
<p>Launch button now appears the moment an install finishes, instead of waiting up to 60 seconds for the next container scan. After a successful install or update, the backend kicks the scanner and waits for a fresh manifest to land before flipping the app to Running, so the UI always has real port and UI-route info by the time the card becomes clickable.</p> <p>Launch button now appears the moment an install finishes, instead of waiting up to 60 seconds for the next container scan. After a successful install or update, the backend kicks the scanner and waits for a fresh manifest to land before flipping the app to Running, so the UI always has real port and UI-route info by the time the card becomes clickable.</p>
<p>Retired the decommissioned .23 Hetzner VPS mirror. New nodes default to OVH (146.59.87.168) as Server 1 and tx1138 as Server 2 for both system updates and the app registry. Existing nodes auto-purge any saved .23 entries on next load so they stop paying connection-timeout penalties against a dead host.</p> <p>Retired the decommissioned Hetzner VPS mirror. New nodes default to the OVH mirror as Server 1 and tx1138 as Server 2 for both system updates and the app registry. Existing nodes auto-purge any saved entries for the dead mirror on next load so they stop paying connection-timeout penalties against a dead host.</p>
<p>Update-available badges and version comparisons work again across every app. The backend was looking for its pinned-image catalog at the wrong path and silently getting an empty result on deployed nodes, which meant the UI never showed "update available" even when a newer image was ready. The search path now matches where the image recipe actually installs the file.</p> <p>Update-available badges and version comparisons work again across every app. The backend was looking for its pinned-image catalog at the wrong path and silently getting an empty result on deployed nodes, which meant the UI never showed "update available" even when a newer image was ready. The search path now matches where the image recipe actually installs the file.</p>
<p>Nodes with a 2 TB data drive are no longer silently configured as pruned Bitcoin nodes. The disk-size check that decides whether to enable pruning was measuring the tiny OS partition instead of the large encrypted data partition, so every archy install with a separate data volume was flipping into prune=550 mode on reconcile and deleting its historical blocks on the next bitcoin-knots restart. The check now measures the actual data partition, so full-archive nodes stay full-archive.</p> <p>Nodes with a 2 TB data drive are no longer silently configured as pruned Bitcoin nodes. The disk-size check that decides whether to enable pruning was measuring the tiny OS partition instead of the large encrypted data partition, so every archy install with a separate data volume was flipping into prune=550 mode on reconcile and deleting its historical blocks on the next bitcoin-knots restart. The check now measures the actual data partition, so full-archive nodes stay full-archive.</p>
<p>Recovery from a failed update no longer leaves a container permanently missing. When an app update failed partway through, the rollback path tried to restart the old container by name even though the forward path had already deleted it, leaving a hole in the node that required manual intervention. The reconcile tool now supports a --create-missing flag that rebuilds any registered container from its canonical spec, giving the update flow a safe recovery path.</p> <p>Recovery from a failed update no longer leaves a container permanently missing. When an app update failed partway through, the rollback path tried to restart the old container by name even though the forward path had already deleted it, leaving a hole in the node that required manual intervention. The reconcile tool now supports a --create-missing flag that rebuilds any registered container from its canonical spec, giving the update flow a safe recovery path.</p>
@ -1226,7 +1226,7 @@ init()
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10"> <div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>App installs now show a real download progress bar same accuracy as the system update bar. You'll see "Downloading: 50.5 / 200.0 MB (25%)" with a live percentage instead of a generic spinner. The bar keeps streaming even when the install falls back from one registry to another, so you'll never see a "stuck at 0%" again.</p> <p>App installs now show a real download progress bar same accuracy as the system update bar. You'll see "Downloading: 50.5 / 200.0 MB (25%)" with a live percentage instead of a generic spinner. The bar keeps streaming even when the install falls back from one registry to another, so you'll never see a "stuck at 0%" again.</p>
<p>Uninstalls now show what's actually happening: "Stopping containers (2/5)", "Cleaning up volumes", "Removing app data" — labelled per app so you can fire off multiple uninstalls in parallel and watch each one's stage on its own card.</p> <p>Uninstalls now show what's actually happening: "Stopping containers (2/5)", "Cleaning up volumes", "Removing app data" — labelled per app so you can fire off multiple uninstalls in parallel and watch each one's stage on its own card.</p>
<p>OVH (146.59.87.168) is now baked in as Server 3 by default for both updates and the app registry extra mirror, completely independent network path so a single-provider outage can't take everything down.</p> <p>The OVH mirror is now baked in as Server 3 by default for both updates and the app registry extra mirror, completely independent network path so a single-provider outage can't take everything down.</p>
</div> </div>
</div> </div>
<!-- v1.7.29-alpha --> <!-- v1.7.29-alpha -->