Compare commits

...
Author SHA1 Message Date
ssmithxandClaude Opus 5.5 33477f284b fix(files): file purchased content into FileBrowser folders again
Every paid download logged "filing into filebrowser/Music/... failed
(non-fatal): Permission denied". The purchase played in-app but never
appeared in Files. FileBrowser's folders belong to its rootless container
range (host uid 100000, mode 755). This service is host uid 1000, outside
that range, so it can read them but not create files in them.

New container::filebrowser::save_new_file:
- Writes directly when the folder allows it.
- Otherwise writes through `podman unshare`, where that uid range is
  ours: to a temp file, then chowned to the folder's owner, set to 0644,
  and hard-linked into place. FileBrowser never sees a partial file and an
  existing file is never replaced. A missing folder is created and given
  its parent's owner. No sudo.
- Keeps the "name (2).ext" de-duplication the RPC did inline.

Checked the unshare script on amishparadise in a scratch folder owned
like FileBrowser's: new folder + file OK, owner/mode right, no clobber,
no temp file left, and the service can read the result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:36:31 +00:00
archipelago 540639d2c1 chore: publish release v1.8.19-alpha 2026-09-28 13:21:51 -04:00
archipelago 562871b1ce chore: prepare signed release v1.8.19-alpha 2026-09-28 13:18:34 -04:00
archipelago cca3f8bfcd docs: include AIUI packaging fix in v1.8.19 release notes
Demo images / Build & push demo images (push) Failing after 44s
2026-09-28 13:13:31 -04:00
archipelago 89c08be712 fix(aiui): match host color scheme for iframe transparency
Demo images / Build & push demo images (push) Failing after 56s
2026-09-28 12:37:40 -04:00
archipelago b4ecf86c13 chore: stage v1.8.19-alpha version bump 2026-09-28 12:33:59 -04:00
archipelago b14fe78306 fix(aiui): expose host wallpaper and package fresh production builds 2026-09-28 12:32:18 -04:00
archipelago 3f0c1038c3 docs: add v1.8.19 release notes to settings 2026-09-28 12:23:57 -04:00
archipelago 1fbefce6df docs: add v1.8.19-alpha release notes 2026-09-28 12:23:05 -04:00
15 changed files with 309 additions and 90 deletions
+6
View File
@@ -2,6 +2,12 @@
## Unreleased
## v1.8.19-alpha (2026-09-28)
- Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.
- Embedded AIUI now stays transparent so the dashboard background appears once.
- AIUI background fixes are now included reliably in OTA updates and fresh installations.
## v1.8.18-alpha (2026-09-18)
- Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.
+3 -2
View File
@@ -46,13 +46,14 @@ interface RateBucket {
const rateBuckets = new Map<string, RateBucket>()
// Clean up stale buckets every 5 minutes
// Vite imports this module during builds too; cleanup must not keep the
// process alive once compilation has finished.
setInterval(() => {
const now = Date.now()
for (const [key, bucket] of rateBuckets) {
if (now > bucket.resetAt) rateBuckets.delete(key)
}
}, 5 * 60_000)
}, 5 * 60_000).unref()
function getClientIp(req: IncomingMessage): string {
return req.socket.remoteAddress ?? 'unknown'
+1
View File
@@ -33,6 +33,7 @@ const PWA_CACHE_VERSION = '2'
// Only embedded when explicitly requested via ?embedded param
const _embeddedFlag = new URLSearchParams(window.location.search).has('embedded')
;(window as unknown as Record<string, unknown>).__AIUI_EMBEDDED__ = _embeddedFlag
document.documentElement.classList.toggle('aiui-embedded', _embeddedFlag)
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
+15 -6
View File
@@ -57,12 +57,8 @@ body {
width: 100%;
height: 100%;
overflow: hidden;
/* Every page paints its own explicit background (bg-[#0a0a0a] / bg-[#faf9f6])
EXCEPT the embedded Chat page, which intentionally goes transparent so
Archy's own dark chrome can show behind it (Chat.vue's iframe host). With
no background-color here, "transparent" fell through to the browser's
default white canvas instead. Match the theme's own dark/light default so
nothing above this ever needs to guess. */
/* Standalone canvas fallback. Embedded mode overrides this below so
Archy's wallpaper remains visible through the iframe. */
background-color: #0a0a0a;
}
@@ -70,6 +66,19 @@ html.light body {
background-color: #faf9f6;
}
/* The host owns the wallpaper when AIUI is embedded. The document canvas
must be transparent too, otherwise it hides the host behind ChatPage. */
html.aiui-embedded {
/* Match Archy's dark canvas scheme. Browsers otherwise give an iframe
with a different scheme an opaque canvas despite transparent CSS. */
color-scheme: dark;
}
html.aiui-embedded,
html.aiui-embedded body {
background: transparent;
}
/* ===== DARK MODE GLASSMORPHISM — from Archy ===== */
@layer components {
+1 -1
View File
@@ -104,7 +104,7 @@ dependencies = [
[[package]]
name = "archipelago"
version = "1.8.18-alpha"
version = "1.8.19-alpha"
dependencies = [
"anyhow",
"archipelago-container",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "archipelago"
version = "1.8.18-alpha"
version = "1.8.19-alpha"
edition = "2021"
license.workspace = true
description = "Archipelago Bitcoin Node OS - Native backend"
+6 -22
View File
@@ -678,28 +678,12 @@ impl RpcHandler {
.unwrap_or("download")
.to_string();
let dir = self.config.data_dir.join("filebrowser").join(folder);
if let Err(e) = tokio::fs::create_dir_all(&dir).await {
tracing::warn!("paid download: cannot create {}: {e}", dir.display());
} else {
// Don't clobber an existing file of the same name: "x.jpg"
// → "x (2).jpg" etc.
let mut target = dir.join(&base);
let (stem, ext) = match base.rsplit_once('.') {
Some((s, e)) if !s.is_empty() => (s.to_string(), format!(".{e}")),
_ => (base.clone(), String::new()),
};
let mut n = 2;
while target.exists() {
target = dir.join(format!("{stem} ({n}){ext}"));
n += 1;
}
match tokio::fs::write(&target, &bytes).await {
Ok(()) => tracing::info!("paid download: filed into {}", target.display()),
Err(e) => tracing::warn!(
"paid download: filing into {} failed (non-fatal): {e}",
target.display()
),
}
match crate::container::filebrowser::save_new_file(&dir, &base, &bytes).await {
Ok(path) => tracing::info!("paid download: filed into {}", path.display()),
Err(e) => tracing::warn!(
"paid download: filing into {} failed (non-fatal): {e:#}",
dir.display()
),
}
}
+209 -1
View File
@@ -5,7 +5,7 @@
//! starting the container with `--config /data/.filebrowser.json`.
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use tokio::fs;
use crate::update::host_sudo;
@@ -117,6 +117,123 @@ fn shell_quote(s: &str) -> String {
s.replace('\'', "'\\''")
}
/// Save `bytes` into FileBrowser's storage as a new file in `dir`, named
/// `name` or, if that's taken, `name (2)`, `name (3)`… Never overwrites.
/// Returns the path written.
///
/// FileBrowser's folders belong to its rootless container range (host uid
/// 100000, mode 755), so this service — host uid 1000, outside that range —
/// can read them but not write into them, and filing a purchase into Files
/// failed with EACCES (2026-09-29). When a direct write is refused, the file
/// is written through `podman unshare`, where that range is ours, and given
/// the folder's owner so FileBrowser manages it like its own uploads.
pub async fn save_new_file(dir: &Path, name: &str, bytes: &[u8]) -> Result<PathBuf> {
save_new_file_with(dir, name, bytes, write_via_userns).await
}
async fn save_new_file_with<F, Fut>(
dir: &Path,
name: &str,
bytes: &[u8],
fallback: F,
) -> Result<PathBuf>
where
F: FnOnce(PathBuf, Vec<u8>) -> Fut,
Fut: std::future::Future<Output = Result<()>>,
{
let target = unused_name(dir, name);
match write_direct(dir, &target, bytes).await {
Ok(()) => Ok(target),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
fallback(target.clone(), bytes.to_vec())
.await
.with_context(|| format!("writing {} via podman unshare", target.display()))?;
Ok(target)
}
Err(e) => Err(e).with_context(|| format!("writing {}", target.display())),
}
}
/// `dir/name`, or the first free `dir/stem (n).ext` from n = 2.
fn unused_name(dir: &Path, name: &str) -> PathBuf {
let mut target = dir.join(name);
let (stem, ext) = match name.rsplit_once('.') {
Some((s, e)) if !s.is_empty() => (s.to_string(), format!(".{e}")),
_ => (name.to_string(), String::new()),
};
let mut n = 2;
while target.exists() {
target = dir.join(format!("{stem} ({n}){ext}"));
n += 1;
}
target
}
async fn write_direct(dir: &Path, target: &Path, bytes: &[u8]) -> std::io::Result<()> {
use tokio::io::AsyncWriteExt;
fs::create_dir_all(dir).await?;
let mut f = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(target)
.await?;
let written = async {
f.write_all(bytes).await?;
f.flush().await
}
.await;
if written.is_err() {
let _ = fs::remove_file(target).await;
}
written
}
/// Write `bytes` (piped on stdin) to `target` from inside the rootless user
/// namespace. It goes to a temp file first and is hard-linked into place, so
/// FileBrowser never sees a partial file and an existing file is never
/// replaced (`ln` refuses an existing name).
async fn write_via_userns(target: PathBuf, bytes: Vec<u8>) -> Result<()> {
use tokio::io::AsyncWriteExt;
const SCRIPT: &str = r#"set -eu
dst=$1
dir=$(dirname -- "$dst")
if [ ! -d "$dir" ]; then
mkdir -- "$dir"
chown --reference="$(dirname -- "$dir")" -- "$dir"
fi
tmp="$dir/.archy-saving.$$"
trap 'rm -f -- "$tmp"' EXIT
cat > "$tmp"
chown --reference="$dir" -- "$tmp"
chmod 0644 -- "$tmp"
ln -- "$tmp" "$dst"
"#;
let mut child = tokio::process::Command::new("podman")
.args(["unshare", "sh", "-c", SCRIPT, "sh"])
.arg(&target)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.spawn()
.context("Failed to run podman unshare")?;
let mut stdin = child.stdin.take().context("podman unshare stdin")?;
let fed = stdin.write_all(&bytes).await;
drop(stdin);
let out = child
.wait_with_output()
.await
.context("Failed to wait for podman unshare")?;
if !out.status.success() {
anyhow::bail!(
"podman unshare exited with {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
);
}
fed.context("Failed to pipe the file to podman unshare")?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -151,4 +268,95 @@ mod tests {
let second = ensure_config(&paths).await.unwrap();
assert_eq!(second, EnsureOutcome::Unchanged);
}
#[test]
fn unused_name_numbers_duplicates_and_keeps_the_extension() {
let dir = tempfile::tempdir().unwrap();
let d = dir.path();
assert_eq!(unused_name(d, "song.mp3"), d.join("song.mp3"));
std::fs::write(d.join("song.mp3"), b"").unwrap();
assert_eq!(unused_name(d, "song.mp3"), d.join("song (2).mp3"));
std::fs::write(d.join("song (2).mp3"), b"").unwrap();
assert_eq!(unused_name(d, "song.mp3"), d.join("song (3).mp3"));
std::fs::write(d.join("README"), b"").unwrap();
assert_eq!(unused_name(d, "README"), d.join("README (2)"));
std::fs::write(d.join(".hidden"), b"").unwrap();
assert_eq!(unused_name(d, ".hidden"), d.join(".hidden (2)"));
}
#[tokio::test]
async fn save_new_file_writes_directly_into_a_writable_folder() {
let dir = tempfile::tempdir().unwrap();
let music = dir.path().join("Music");
let path = save_new_file_with(&music, "a.mp3", b"abc", |_, _| async {
anyhow::bail!("fallback must not run")
})
.await
.unwrap();
assert_eq!(path, music.join("a.mp3"));
assert_eq!(std::fs::read(&path).unwrap(), b"abc");
}
#[tokio::test]
async fn save_new_file_never_overwrites_an_existing_file() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("a.mp3"), b"original").unwrap();
let path = save_new_file_with(dir.path(), "a.mp3", b"new", |_, _| async {
anyhow::bail!("fallback must not run")
})
.await
.unwrap();
assert_eq!(path, dir.path().join("a (2).mp3"));
assert_eq!(
std::fs::read(dir.path().join("a.mp3")).unwrap(),
b"original"
);
}
/// Regression (2026-09-29): filing a purchase into a FileBrowser folder
/// owned by the container's uid range failed with EACCES. A refused
/// write must go through the user-namespace fallback, with the same
/// target and bytes.
#[tokio::test]
async fn a_refused_write_goes_through_the_userns_fallback() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let music = dir.path().join("Music");
std::fs::create_dir(&music).unwrap();
std::fs::set_permissions(&music, std::fs::Permissions::from_mode(0o555)).unwrap();
if std::fs::File::create(music.join("probe")).is_ok() {
return; // running as root: mode bits don't refuse the write
}
let seen = std::sync::Mutex::new(None);
let path = save_new_file_with(&music, "a.mp3", b"abc", |target, bytes| {
*seen.lock().unwrap() = Some((target, bytes));
async { Ok(()) }
})
.await
.unwrap();
assert_eq!(path, music.join("a.mp3"));
assert_eq!(
seen.into_inner().unwrap(),
Some((music.join("a.mp3"), b"abc".to_vec()))
);
std::fs::set_permissions(&music, std::fs::Permissions::from_mode(0o755)).unwrap();
}
#[tokio::test]
async fn a_failed_fallback_is_reported() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o555)).unwrap();
if std::fs::File::create(dir.path().join("probe")).is_ok() {
return;
}
let err = save_new_file_with(dir.path(), "a.mp3", b"abc", |_, _| async {
anyhow::bail!("no podman")
})
.await
.unwrap_err();
assert!(format!("{err:#}").contains("no podman"));
std::fs::set_permissions(dir.path(), std::fs::Permissions::from_mode(0o755)).unwrap();
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "neode-ui",
"version": "1.8.18-alpha",
"version": "1.8.19-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "neode-ui",
"version": "1.8.18-alpha",
"version": "1.8.19-alpha",
"dependencies": {
"@scure/bip39": "^2.2.0",
"@types/dompurify": "^3.0.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "neode-ui",
"private": true,
"version": "1.8.18-alpha",
"version": "1.8.19-alpha",
"type": "module",
"scripts": {
"start": "./start-dev.sh",
@@ -362,6 +362,18 @@ init()
</button>
</div>
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
<!-- v1.8.19-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.19-alpha</span>
<span class="text-xs text-white/40">September 28, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.</p>
<p>Embedded AIUI now stays transparent so the dashboard background appears once.</p>
<p>AIUI background fixes are now included reliably in OTA updates and fresh installations.</p>
</div>
</div>
<!-- v1.8.18-alpha -->
<div>
<div class="flex items-center gap-2 mb-3">
@@ -369,9 +381,9 @@ init()
<span class="text-xs text-white/40">September 18, 2026</span>
</div>
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
<p>Framework startup now brings Bitcoin and LND up before unrelated containers, and the dashboard keeps balances unavailable instead of showing a false zero when LND is not ready.</p>
<p>Cashu wallets can set up a recovery phrase from Receive, with clearer backup and restore guidance.</p>
<p>Ecash backup guidance is shorter and arranged in a single column for easier use on small screens.</p>
<p>Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.</p>
<p>Cashu Receive guides unseeded wallets through recovery-phrase setup, with shorter backup guidance and a single-column layout.</p>
<p>Added live Framework verification for automatic LND unlock, native balance preservation, Cashu address registration, and proof preservation.</p>
</div>
</div>
<!-- v1.8.17-alpha -->
+17 -17
View File
@@ -1,29 +1,29 @@
{
"changelog": [
"Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.",
"Cashu Receive guides unseeded wallets through recovery-phrase setup, with shorter backup guidance and a single-column layout.",
"Added live Framework verification for automatic LND unlock, native balance preservation, Cashu address registration, and proof preservation."
"Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.",
"Embedded AIUI now stays transparent so the dashboard background appears once.",
"AIUI background fixes are now included reliably in OTA updates and fresh installations."
],
"components": [
{
"current_version": "1.8.18-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago",
"current_version": "1.8.19-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago",
"name": "archipelago",
"new_version": "1.8.18-alpha",
"sha256": "c76415c295dd42159e45e74d93f7c2f6fa39b0351946a8303f8cfba1dd3a6087",
"size_bytes": 64497240
"new_version": "1.8.19-alpha",
"sha256": "bb500d02567ad16179179d43138cc6fdafee680835262026eeaa7d1bc8cdd307",
"size_bytes": 64495784
},
{
"current_version": "1.8.18-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago-frontend-1.8.18-alpha.tar.gz",
"name": "archipelago-frontend-1.8.18-alpha.tar.gz",
"new_version": "1.8.18-alpha",
"sha256": "57da88b1a8f3d2841867994a95e9e4e728c19616abcde8e3d3f151c6d94ed501",
"size_bytes": 98801020
"current_version": "1.8.19-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago-frontend-1.8.19-alpha.tar.gz",
"name": "archipelago-frontend-1.8.19-alpha.tar.gz",
"new_version": "1.8.19-alpha",
"sha256": "fbbaa237e2ea4e9e576dda9b15fdcf3c59c40bc55bfee4ebbea303b0172fc49b",
"size_bytes": 97142031
}
],
"release_date": "2026-09-20",
"signature": "84e0e1a20750f0d4ff27ba539c2e9424a5b02c19a5689f73cdf82813212acae19e0af244a62e66ac37a35d97d80deb2833819d809b91a7822f2bfd785d3fe30d",
"release_date": "2026-09-28",
"signature": "4da9bf766d94c2de6641619a36663106791c7a1d342b729c666662cdde1feabdf11c51994c1cbbe2a0e2b270c316d77763435e976ce22730855b3822bc9d390a",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.18-alpha"
"version": "1.8.19-alpha"
}
+17 -17
View File
@@ -1,29 +1,29 @@
{
"changelog": [
"Framework startup prioritizes Bitcoin and LND before unrelated containers, and unavailable LND balances remain unavailable instead of appearing as false zeroes.",
"Cashu Receive guides unseeded wallets through recovery-phrase setup, with shorter backup guidance and a single-column layout.",
"Added live Framework verification for automatic LND unlock, native balance preservation, Cashu address registration, and proof preservation."
"Fixed the embedded AIUI chat page painting a second background and dark scrim over Archy’s dashboard background.",
"Embedded AIUI now stays transparent so the dashboard background appears once.",
"AIUI background fixes are now included reliably in OTA updates and fresh installations."
],
"components": [
{
"current_version": "1.8.18-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago",
"current_version": "1.8.19-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago",
"name": "archipelago",
"new_version": "1.8.18-alpha",
"sha256": "c76415c295dd42159e45e74d93f7c2f6fa39b0351946a8303f8cfba1dd3a6087",
"size_bytes": 64497240
"new_version": "1.8.19-alpha",
"sha256": "bb500d02567ad16179179d43138cc6fdafee680835262026eeaa7d1bc8cdd307",
"size_bytes": 64495784
},
{
"current_version": "1.8.18-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.18-alpha/archipelago-frontend-1.8.18-alpha.tar.gz",
"name": "archipelago-frontend-1.8.18-alpha.tar.gz",
"new_version": "1.8.18-alpha",
"sha256": "57da88b1a8f3d2841867994a95e9e4e728c19616abcde8e3d3f151c6d94ed501",
"size_bytes": 98801020
"current_version": "1.8.19-alpha",
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.19-alpha/archipelago-frontend-1.8.19-alpha.tar.gz",
"name": "archipelago-frontend-1.8.19-alpha.tar.gz",
"new_version": "1.8.19-alpha",
"sha256": "fbbaa237e2ea4e9e576dda9b15fdcf3c59c40bc55bfee4ebbea303b0172fc49b",
"size_bytes": 97142031
}
],
"release_date": "2026-09-20",
"signature": "84e0e1a20750f0d4ff27ba539c2e9424a5b02c19a5689f73cdf82813212acae19e0af244a62e66ac37a35d97d80deb2833819d809b91a7822f2bfd785d3fe30d",
"release_date": "2026-09-28",
"signature": "4da9bf766d94c2de6641619a36663106791c7a1d342b729c666662cdde1feabdf11c51994c1cbbe2a0e2b270c316d77763435e976ce22730855b3822bc9d390a",
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
"version": "1.8.18-alpha"
"version": "1.8.19-alpha"
}
+10 -8
View File
@@ -78,15 +78,17 @@ if [ -z "$FRONTEND_ARCHIVE" ]; then
STAGING_DIR=$(mktemp -d -t archipelago-frontend.XXXXXX)
echo "Staging frontend archive in $STAGING_DIR..."
cp -r "$FRONTEND_DIST/." "$STAGING_DIR/"
# Bake AIUI in so fresh installs pick it up. OTA already
# carries-forward the existing aiui/ if the tarball lacks one
# (update.rs:922), but including it here makes the tarball
# the single source of truth instead of relying on a side-
# effect of the in-place swap.
if [ -d "$PROJECT_ROOT/demo/aiui" ] && [ -f "$PROJECT_ROOT/demo/aiui/index.html" ]; then
echo " Including AIUI from demo/aiui/"
cp -r "$PROJECT_ROOT/demo/aiui" "$STAGING_DIR/aiui"
# create-release.sh folds the freshly built AIUI into FRONTEND_DIST.
# Never overlay it with the older demo bundle (or nest aiui/aiui/).
if [ ! -f "$STAGING_DIR/aiui/index.html" ] || \
[ ! -f "$STAGING_DIR/aiui/BUILD-INFO" ]; then
echo "Error: fresh AIUI payload missing from frontend dist" >&2
exit 1
fi
grep -Fxq "commit=$(git -C "$PROJECT_ROOT" rev-parse HEAD)" "$STAGING_DIR/aiui/BUILD-INFO" || {
echo "Error: AIUI payload was not built from the current commit" >&2
exit 1
}
# OTA bridge for nodes running older updaters: they only know how to
# apply the backend binary and frontend archive. Carry host runtime
# assets inside the frontend tarball; the new backend promotes them
+5 -9
View File
@@ -169,15 +169,11 @@ else
fi
cd "$PROJECT_ROOT"
# npm run build wipes web/dist — fold AIUI straight back in. The OTA tarball
# bakes it from demo/aiui independently, but build-iso-release.sh's
# verify-artifacts guard checks web/dist/neode-ui/aiui and failed on two
# consecutive releases (.127, .129) because this fold-in was manual.
if [ -d "$PROJECT_ROOT/demo/aiui" ] && [ -f "$PROJECT_ROOT/demo/aiui/index.html" ]; then
rm -rf "$PROJECT_ROOT/web/dist/neode-ui/aiui"
cp -r "$PROJECT_ROOT/demo/aiui" "$PROJECT_ROOT/web/dist/neode-ui/aiui"
echo " AIUI folded into web/dist from demo/aiui"
fi
# Build AIUI from the same source as the release. The checked-in demo bundle
# can predate source fixes and must never overwrite the production payload.
bash "$SCRIPT_DIR/build-aiui.sh"
rm -rf "$PROJECT_ROOT/web/dist/neode-ui/aiui"
cp -r "$PROJECT_ROOT/aiui/packages/app/dist" "$PROJECT_ROOT/web/dist/neode-ui/aiui"
# npm run build can silently no-op (vue-tsc EACCES burned us before) — a stale
# dist would ship with a perfectly valid sha256. Require the freshly built