Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14feb1feb9 | ||
|
|
d2642856c1 | ||
|
|
338bfd43a7 | ||
|
|
3da1d0b0f7 | ||
|
|
500aebb3e2 |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v1.7.118-alpha (2026-07-29)
|
||||
|
||||
- Fixes mesh radios dropping off on nodes that took the v1.7.117 update. Updates only ever replaced the main program, never the packaged radio helpers — so updated nodes were left running an older radio daemon that didn't understand a new option and quietly gave up, showing "device not connected" with a Connect button that did nothing. The node now checks what its radio daemon supports before using new options, and updates finally carry the radio helpers themselves, so every node gets current radio support with the update instead of only from a fresh install.
|
||||
- The in-app "Flash LoRa" flow works on updated nodes. The RNode flashing tool was only ever included on freshly installed nodes; everywhere else flashing failed with a cryptic "No such file or directory". The tool now ships with updates and is included on new install images, and if it's somehow still missing the error says exactly what to do instead.
|
||||
- Message notification badges finally remember what you've read. Unread counts were only kept in memory, so every visit re-counted old messages as new — including a phantom badge for chats with nothing new in them. Read-state is now saved on the device, opening a chat marks all its linked conversations read, and history no longer re-badges after a reload.
|
||||
- Every mesh message now has a visible "⋯" button that opens the route view: watch the path your message took animate — sender and receiver appear, a pulse travels the link, and each relay hop lights up in order — with signal quality for radio links and delivery status. (Tapping the transport pill still works too.)
|
||||
|
||||
## v1.7.117-alpha (2026-07-29)
|
||||
|
||||
- Flash your LoRa radio from inside the app. The Mesh page now has a "Flash LoRa" button that opens a guided flow: pick the firmware family (MeshCore, Meshtastic, or Reticulum RNode) and your board, and the node downloads the latest release and flashes it with live progress — no external flasher website, no cables to a computer. The same flow appears when a freshly plugged-in radio is detected, and a long list of flashing pitfalls was fixed along the way: radios no longer boot-loop after a flash, failures show the real error instead of silently bouncing back, wedged flash jobs can't get stuck forever, and board auto-detection no longer misidentifies Heltec boards.
|
||||
|
||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.116-alpha"
|
||||
version = "1.7.117-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "archipelago"
|
||||
version = "1.7.117-alpha"
|
||||
version = "1.7.118-alpha"
|
||||
edition = "2021"
|
||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||
authors = ["Archipelago Team"]
|
||||
|
||||
@@ -373,6 +373,42 @@ async fn run_runtime_assets() -> Result<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
// Packaged radio tools (v1.7.118+): OTA updates only apply the backend
|
||||
// binary and frontend tarball, so these PyInstaller binaries ride the
|
||||
// runtime payload and get promoted here. Without this, fleet nodes keep
|
||||
// a stale archy-reticulum-daemon (which exits on flags it doesn't know —
|
||||
// the v1.7.117 --enable-transport rollout killed their mesh sessions)
|
||||
// and never receive archy-rnodeconf at all (Flash LoRa: "No such file
|
||||
// or directory"). Skipped when byte-identical; a running daemon is
|
||||
// unaffected (install replaces the inode) and picks the new binary up
|
||||
// on its next spawn.
|
||||
for tool in ["archy-reticulum-daemon", "archy-rnodeconf"] {
|
||||
let src = runtime_dir.join("radio-tools").join(tool);
|
||||
if !src.exists() {
|
||||
continue;
|
||||
}
|
||||
let dest = format!("/usr/local/bin/{}", tool);
|
||||
let same = match (fs::read(&src).await, fs::read(&dest).await) {
|
||||
(Ok(a), Ok(b)) => a == b,
|
||||
_ => false,
|
||||
};
|
||||
if same {
|
||||
continue;
|
||||
}
|
||||
let src_s = src.to_string_lossy().to_string();
|
||||
let status = host_sudo(&["install", "-m", "755", &src_s, &dest])
|
||||
.await
|
||||
.with_context(|| format!("install {}", tool))?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("install {} exited with {}", tool, status);
|
||||
}
|
||||
info!(
|
||||
tool,
|
||||
"Promoted packaged radio tool from OTA runtime payload"
|
||||
);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if changed {
|
||||
let _ = host_sudo(&["systemctl", "daemon-reload"]).await;
|
||||
if nginx_src.exists() {
|
||||
|
||||
@@ -823,6 +823,18 @@ fn rnodeconf_bin() -> String {
|
||||
.unwrap_or_else(|_| "/usr/local/bin/archy-rnodeconf".to_string())
|
||||
}
|
||||
|
||||
/// True when `name` resolves to an executable on PATH.
|
||||
fn which_on_path(name: &str) -> bool {
|
||||
std::env::var_os("PATH")
|
||||
.map(|paths| {
|
||||
std::env::split_paths(&paths).any(|dir| {
|
||||
let candidate = dir.join(name);
|
||||
candidate.is_file()
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// `--autoinstall`'s "which board is this" step is interactive by design —
|
||||
/// confirmed live against a real Heltec V4 (2026-07-23): even with a board
|
||||
/// given on the command line, rnodeconf can't always tell V3 from V4 apart
|
||||
@@ -877,9 +889,17 @@ async fn rnodeconf_autoinstall(
|
||||
let bin = rnodeconf_bin();
|
||||
let mut cmd = if Path::new(&bin).exists() {
|
||||
Command::new(bin)
|
||||
} else {
|
||||
} else if which_on_path("rnodeconf") {
|
||||
// Dev fallback if only a plain venv/system rnodeconf is on PATH.
|
||||
Command::new("rnodeconf")
|
||||
} else {
|
||||
// Older ISOs/OTAs never shipped the tool — say so instead of the
|
||||
// bare "No such file or directory" the spawn would produce.
|
||||
anyhow::bail!(
|
||||
"{bin} is not installed on this node — RNode flashing needs the packaged \
|
||||
archy-rnodeconf tool, which ships with the v1.7.118+ update (or can be \
|
||||
sideloaded from a dev box). Update the node, then retry."
|
||||
);
|
||||
};
|
||||
cmd.args(["--autoinstall", path]);
|
||||
let stdin = format!(
|
||||
|
||||
@@ -115,6 +115,59 @@ fn is_loopback_host(host: &str) -> bool {
|
||||
/// Reticulum-carried identity binds onto the existing Archy contact via the
|
||||
/// existing `parse_identity_broadcast`/`handle_identity_received` path,
|
||||
/// satisfying cross-protocol DM convergence with zero new Rust dispatch code.
|
||||
/// Resolve the daemon invocation: packaged binary, else the dev venv script.
|
||||
/// `(program, Some(script_arg))` for the venv fallback.
|
||||
fn daemon_program() -> (String, Option<String>) {
|
||||
let bin = std::env::var("ARCHY_RETICULUM_DAEMON_BIN")
|
||||
.unwrap_or_else(|_| "/usr/local/bin/archy-reticulum-daemon".to_string());
|
||||
if Path::new(&bin).exists() {
|
||||
return (bin, None);
|
||||
}
|
||||
let py = std::env::var("ARCHY_RETICULUM_DAEMON_PY")
|
||||
.unwrap_or_else(|_| "reticulum-daemon/.venv/bin/python".to_string());
|
||||
let script = std::env::var("ARCHY_RETICULUM_DAEMON_SCRIPT")
|
||||
.unwrap_or_else(|_| "reticulum-daemon/reticulum_daemon.py".to_string());
|
||||
(py, Some(script))
|
||||
}
|
||||
|
||||
/// Whether the installed daemon understands `--enable-transport`. OTA updates
|
||||
/// ship the archipelago binary ahead of the packaged daemon tools, so a fleet
|
||||
/// node can run a new binary against an old daemon — whose argparse EXITS on
|
||||
/// an unknown flag, killing the mesh session on every spawn (live regression,
|
||||
/// framework-pt on v1.7.117). Probe `--help` and only pass the flag when the
|
||||
/// daemon advertises it; an old daemon then runs edge-only exactly as before.
|
||||
async fn daemon_supports_enable_transport() -> bool {
|
||||
let (program, script) = daemon_program();
|
||||
let mut cmd = Command::new(&program);
|
||||
if let Some(script) = &script {
|
||||
cmd.arg(script);
|
||||
}
|
||||
cmd.arg("--help")
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null());
|
||||
let out = tokio::time::timeout(Duration::from_secs(10), async { cmd.output().await }).await;
|
||||
match out {
|
||||
Ok(Ok(out)) => {
|
||||
let supported = String::from_utf8_lossy(&out.stdout).contains("--enable-transport");
|
||||
if !supported {
|
||||
warn!(
|
||||
program,
|
||||
"installed reticulum-daemon predates --enable-transport — running edge-only until the daemon tools update"
|
||||
);
|
||||
}
|
||||
supported
|
||||
}
|
||||
_ => {
|
||||
warn!(
|
||||
program,
|
||||
"reticulum-daemon --help probe failed — not passing --enable-transport"
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn daemon_command(
|
||||
socket_path: &Path,
|
||||
iface: &ReticulumInterface<'_>,
|
||||
@@ -122,21 +175,13 @@ fn daemon_command(
|
||||
archy_ed_pubkey_hex: Option<&str>,
|
||||
archy_x25519_pubkey_hex: Option<&str>,
|
||||
display_name: Option<&str>,
|
||||
enable_transport: bool,
|
||||
) -> Command {
|
||||
let bin = std::env::var("ARCHY_RETICULUM_DAEMON_BIN")
|
||||
.unwrap_or_else(|_| "/usr/local/bin/archy-reticulum-daemon".to_string());
|
||||
let mut cmd = if Path::new(&bin).exists() {
|
||||
Command::new(bin)
|
||||
} else {
|
||||
// Dev fallback: run the script through its venv interpreter.
|
||||
let py = std::env::var("ARCHY_RETICULUM_DAEMON_PY")
|
||||
.unwrap_or_else(|_| "reticulum-daemon/.venv/bin/python".to_string());
|
||||
let script = std::env::var("ARCHY_RETICULUM_DAEMON_SCRIPT")
|
||||
.unwrap_or_else(|_| "reticulum-daemon/reticulum_daemon.py".to_string());
|
||||
let mut c = Command::new(py);
|
||||
c.arg(script);
|
||||
c
|
||||
};
|
||||
let (program, script) = daemon_program();
|
||||
let mut cmd = Command::new(program);
|
||||
if let Some(script) = script {
|
||||
cmd.arg(script);
|
||||
}
|
||||
cmd.arg("--identity-key")
|
||||
.arg(identity_key)
|
||||
.arg("--socket")
|
||||
@@ -158,8 +203,11 @@ fn daemon_command(
|
||||
// announces, so archy nodes (and Sideband/NomadNet peers) beyond direct RF
|
||||
// range discover and reach each other through any archy node in between.
|
||||
// Edge-only operation (`enable_transport = no`) left every node an island
|
||||
// limited to its own radio horizon.
|
||||
cmd.arg("--enable-transport");
|
||||
// limited to its own radio horizon. Gated on the installed daemon actually
|
||||
// supporting the flag — see `daemon_supports_enable_transport`.
|
||||
if enable_transport {
|
||||
cmd.arg("--enable-transport");
|
||||
}
|
||||
if let (Some(ed), Some(x)) = (archy_ed_pubkey_hex, archy_x25519_pubkey_hex) {
|
||||
cmd.arg("--archy-ed-pubkey-hex")
|
||||
.arg(ed)
|
||||
@@ -405,6 +453,7 @@ impl ReticulumLink {
|
||||
);
|
||||
}
|
||||
|
||||
let enable_transport = daemon_supports_enable_transport().await;
|
||||
let mut cmd = daemon_command(
|
||||
&socket_path,
|
||||
&iface,
|
||||
@@ -412,6 +461,7 @@ impl ReticulumLink {
|
||||
our_ed_pubkey_hex,
|
||||
our_x25519_pubkey_hex,
|
||||
display_name,
|
||||
enable_transport,
|
||||
);
|
||||
cmd.env("TMPDIR", &tmp_dir);
|
||||
let child = cmd
|
||||
@@ -1500,6 +1550,45 @@ mod tests {
|
||||
assert_eq!(reticulum_contact_id_from_hash(&zero_hash), 1);
|
||||
}
|
||||
|
||||
// One test covers both directions to avoid two tests racing on the same
|
||||
// process-global env var under the parallel test runner.
|
||||
#[tokio::test]
|
||||
async fn transport_flag_gated_on_daemon_support() {
|
||||
let dir = std::env::temp_dir().join(format!("archy-daemon-stub-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
|
||||
let old = dir.join("old-daemon");
|
||||
std::fs::write(&old, "#!/bin/sh\necho 'usage: --serial-port --no-radio'\n").unwrap();
|
||||
let new = dir.join("new-daemon");
|
||||
std::fs::write(
|
||||
&new,
|
||||
"#!/bin/sh\necho 'usage: --serial-port --enable-transport --no-radio'\n",
|
||||
)
|
||||
.unwrap();
|
||||
for p in [&old, &new] {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(p, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
|
||||
// An old daemon whose --help doesn't advertise the flag: never pass it
|
||||
// (its argparse would exit and kill the mesh session — the v1.7.117
|
||||
// fleet regression this guards against).
|
||||
std::env::set_var("ARCHY_RETICULUM_DAEMON_BIN", &old);
|
||||
assert!(!daemon_supports_enable_transport().await);
|
||||
|
||||
std::env::set_var("ARCHY_RETICULUM_DAEMON_BIN", &new);
|
||||
assert!(daemon_supports_enable_transport().await);
|
||||
|
||||
// A missing binary must also fail safe (probe error → no flag).
|
||||
std::env::set_var("ARCHY_RETICULUM_DAEMON_BIN", dir.join("missing"));
|
||||
// (falls through to the venv dev path, which doesn't exist in the
|
||||
// test environment either → probe fails → false)
|
||||
assert!(!daemon_supports_enable_transport().await);
|
||||
|
||||
std::env::remove_var("ARCHY_RETICULUM_DAEMON_BIN");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn synthetic_frame_matches_meshtastic_layout() {
|
||||
let prefix = [1, 2, 3, 4, 5, 6];
|
||||
|
||||
@@ -1184,6 +1184,19 @@ else
|
||||
echo " ⚠️ archy-reticulum-daemon not found at $RETICULUM_DAEMON — ISO nodes won't support RNode radios until it's sideloaded"
|
||||
fi
|
||||
|
||||
# archy-rnodeconf drives the in-app "Flash LoRa" flow for RNode firmware
|
||||
# (mesh/flash.rs spawns /usr/local/bin/archy-rnodeconf --autoinstall). A node
|
||||
# imaged without it fails every RNode flash with "No such file or directory"
|
||||
# (framework-pt, 2026-07-29, v1.7.117).
|
||||
RNODECONF="${ARCHY_RNODECONF:-/usr/local/bin/archy-rnodeconf}"
|
||||
if [ -f "$RNODECONF" ]; then
|
||||
cp "$RNODECONF" "$ARCH_DIR/bin/archy-rnodeconf"
|
||||
chmod +x "$ARCH_DIR/bin/archy-rnodeconf"
|
||||
echo " ✅ rnodeconf bundled ($(du -h "$ARCH_DIR/bin/archy-rnodeconf" | cut -f1))"
|
||||
else
|
||||
echo " ⚠️ archy-rnodeconf not found at $RNODECONF — ISO nodes can't flash RNode firmware until it's sideloaded"
|
||||
fi
|
||||
|
||||
if [ "$BACKEND_CAPTURED" = "0" ]; then
|
||||
if [ "$BUILD_FROM_SOURCE" != "1" ]; then
|
||||
echo " ⚠️ Could not capture from live server, building from source..."
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.117-alpha",
|
||||
"version": "1.7.118-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "neode-ui",
|
||||
"version": "1.7.117-alpha",
|
||||
"version": "1.7.118-alpha",
|
||||
"dependencies": {
|
||||
"@scure/bip39": "^2.2.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "neode-ui",
|
||||
"private": true,
|
||||
"version": "1.7.117-alpha",
|
||||
"version": "1.7.118-alpha",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "./start-dev.sh",
|
||||
|
||||
@@ -281,6 +281,27 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
|
||||
// Track unread message counts per peer (contact_id -> count)
|
||||
const unreadCounts = ref<Record<number, number>>({})
|
||||
// Durable seen-state: highest message id the user has actually seen, per
|
||||
// contact. Without this, every page load replayed ALL historical received
|
||||
// messages into unreadCounts (the in-memory store starts empty, so every
|
||||
// old message looked "new") — badges came back on every visit. Message ids
|
||||
// are safe watermarks: the backend allocates them monotonically and
|
||||
// restores the counter as max(persisted)+1 across restarts.
|
||||
const LAST_SEEN_KEY = 'archipelago.mesh.last-seen.v1'
|
||||
const lastSeenId = ref<Record<number, number>>(
|
||||
JSON.parse(localStorage.getItem(LAST_SEEN_KEY) || '{}') as Record<number, number>
|
||||
)
|
||||
// First run after this feature ships: treat existing history as seen so
|
||||
// nobody gets a wall of phantom badges for months-old messages.
|
||||
let seedLastSeenFromHistory = localStorage.getItem(LAST_SEEN_KEY) === null
|
||||
function persistLastSeen() {
|
||||
localStorage.setItem(LAST_SEEN_KEY, JSON.stringify(lastSeenId.value))
|
||||
}
|
||||
function advanceLastSeen(contactId: number, msgId: number): boolean {
|
||||
if ((lastSeenId.value[contactId] ?? 0) >= msgId) return false
|
||||
lastSeenId.value[contactId] = msgId
|
||||
return true
|
||||
}
|
||||
// Contact ids of the chat currently on screen — ALL twins of the merged
|
||||
// conversation, not just the clicked row's id, since the unread badge sums
|
||||
// across every underlying contact_id (a message can land on any twin).
|
||||
@@ -455,19 +476,35 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
method: 'mesh.messages',
|
||||
params: limit ? { limit } : {},
|
||||
})
|
||||
// Detect new incoming messages and increment unread counts
|
||||
if (seedLastSeenFromHistory && res.messages.length > 0) {
|
||||
for (const m of res.messages) {
|
||||
if (m.direction === 'received') advanceLastSeen(m.peer_contact_id, m.id)
|
||||
}
|
||||
persistLastSeen()
|
||||
seedLastSeenFromHistory = false
|
||||
}
|
||||
// Detect new incoming messages and increment unread counts. "New" means
|
||||
// past the durable seen watermark, not merely absent from the in-memory
|
||||
// store — otherwise a page reload re-badges the whole history.
|
||||
const newMsgs = res.messages.filter(
|
||||
m => m.direction === 'received' && !messages.value.some(existing => existing.id === m.id)
|
||||
m =>
|
||||
m.direction === 'received' &&
|
||||
m.id > (lastSeenId.value[m.peer_contact_id] ?? 0) &&
|
||||
!messages.value.some(existing => existing.id === m.id)
|
||||
)
|
||||
let seenDirty = false
|
||||
for (const msg of newMsgs) {
|
||||
// Don't count as unread if we're currently viewing that chat AND the
|
||||
// bottom (latest messages) is in view — i.e. the user actually sees it.
|
||||
const seenLive =
|
||||
viewingChatIds.value.includes(msg.peer_contact_id) && viewingAtBottom.value
|
||||
if (!seenLive) {
|
||||
if (seenLive) {
|
||||
seenDirty = advanceLastSeen(msg.peer_contact_id, msg.id) || seenDirty
|
||||
} else {
|
||||
unreadCounts.value[msg.peer_contact_id] = (unreadCounts.value[msg.peer_contact_id] || 0) + 1
|
||||
}
|
||||
}
|
||||
if (seenDirty) persistLastSeen()
|
||||
messages.value = res.messages
|
||||
// Extract node positions from coordinate messages
|
||||
updateNodePositionsFromMessages(res.messages)
|
||||
@@ -595,7 +632,17 @@ export const useMeshStore = defineStore('mesh', () => {
|
||||
function markChatRead(contactId: number | number[]) {
|
||||
const ids = Array.isArray(contactId) ? contactId : [contactId]
|
||||
viewingChatIds.value = ids
|
||||
for (const id of ids) delete unreadCounts.value[id]
|
||||
let seenDirty = false
|
||||
for (const id of ids) {
|
||||
delete unreadCounts.value[id]
|
||||
// Persist the watermark so the seen-state survives reloads.
|
||||
for (const m of messages.value) {
|
||||
if (m.direction === 'received' && m.peer_contact_id === id) {
|
||||
seenDirty = advanceLastSeen(id, m.id) || seenDirty
|
||||
}
|
||||
}
|
||||
}
|
||||
if (seenDirty) persistLastSeen()
|
||||
}
|
||||
|
||||
function clearViewingChat() {
|
||||
|
||||
@@ -2344,6 +2344,7 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
||||
<div v-else class="mesh-chat-bubble-text">{{ msg.plaintext }}</div>
|
||||
<div class="mesh-chat-bubble-meta">
|
||||
<span v-if="transportLabel(msg)" class="mesh-chat-transport mesh-chat-transport-clickable" :class="'transport-' + msg.transport" :title="'Delivered over ' + transportLabel(msg) + ' — click for route details'" @click.stop="hopVizMsg = msg">{{ transportLabel(msg) }}</span>
|
||||
<button class="mesh-chat-more-btn" title="How this message traveled" aria-label="Show message route" @click.stop="hopVizMsg = msg">⋯</button>
|
||||
<span v-if="msg.encrypted" class="mesh-chat-e2e">E2E</span>
|
||||
<span v-if="isEditedMessage(msg) !== null" class="mesh-chat-edited">(edited)</span>
|
||||
<span v-if="msg.delivered && msg.direction === 'sent'" class="mesh-chat-ack">✓✓</span>
|
||||
@@ -2629,7 +2630,7 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
||||
<Teleport to="body">
|
||||
<div v-if="hopVizMsg" class="mesh-transport-modal-backdrop" @click.self="hopVizMsg = null">
|
||||
<div class="glass-card mesh-transport-modal">
|
||||
<h3 class="mesh-transport-title">{{ transportLabel(hopVizMsg) }} route</h3>
|
||||
<h3 class="mesh-transport-title">{{ transportLabel(hopVizMsg) || 'Message' }} route</h3>
|
||||
<p class="mesh-transport-sub">
|
||||
{{ hopVizMsg.direction === 'sent' ? 'You → ' + (hopVizPeer?.advert_name || hopVizMsg.peer_name || 'peer') : (hopVizPeer?.advert_name || hopVizMsg.peer_name || 'peer') + ' → You' }}
|
||||
</p>
|
||||
@@ -2644,6 +2645,9 @@ async function downloadAttachment(payload: MeshAttachmentPayload) {
|
||||
<template v-else-if="hopVizMsg.transport === 'fips'">
|
||||
<div class="mesh-hopviz-link">⚡ FIPS overlay · direct peer-to-peer</div>
|
||||
</template>
|
||||
<template v-else-if="!hopVizMsg.transport">
|
||||
<div class="mesh-hopviz-link">🛰 transport wasn't recorded for this message</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="mesh-hopviz-link">
|
||||
📡 {{ hopVizHops() === null || hopVizHops() === 0 ? 'direct radio link' : `${hopVizHops()} hop${hopVizHops() === 1 ? '' : 's'}` }}
|
||||
|
||||
@@ -595,6 +595,38 @@ select.mesh-bitcoin-input option { background: #1a1a2e; color: rgba(255,255,255,
|
||||
.mesh-chat-transport-clickable { cursor: pointer; }
|
||||
.mesh-chat-transport-clickable:hover { filter: brightness(1.4); }
|
||||
.mesh-hopviz-chain { display: flex; align-items: center; gap: 10px; justify-content: center; padding: 14px 8px; flex-wrap: wrap; }
|
||||
/* Animated route reveal: endpoints and link fade/slide in sequence, then a
|
||||
pulse travels the link continuously to show the direction of travel. */
|
||||
.mesh-hopviz-chain > * { opacity: 0; animation: mesh-hopviz-appear 0.4s ease forwards; }
|
||||
.mesh-hopviz-chain > *:nth-child(1) { animation-delay: 0.05s; }
|
||||
.mesh-hopviz-chain > *:nth-child(2) { animation-delay: 0.35s; }
|
||||
.mesh-hopviz-chain > *:nth-child(3) { animation-delay: 0.65s; }
|
||||
@keyframes mesh-hopviz-appear { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||
.mesh-hopviz-link { position: relative; overflow: hidden; }
|
||||
.mesh-hopviz-link::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -18%;
|
||||
width: 18%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, rgba(251,146,60,0.95), transparent);
|
||||
animation: mesh-hopviz-travel 1.6s linear infinite;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes mesh-hopviz-travel { from { left: -18%; } to { left: 100%; } }
|
||||
.mesh-hopviz-dot { animation: mesh-hopviz-blink 1.6s ease-in-out infinite; }
|
||||
.mesh-hopviz-dot:nth-child(2) { animation-delay: 0.25s; }
|
||||
.mesh-hopviz-dot:nth-child(3) { animation-delay: 0.5s; }
|
||||
.mesh-hopviz-dot:nth-child(4) { animation-delay: 0.75s; }
|
||||
.mesh-hopviz-dot:nth-child(5) { animation-delay: 1s; }
|
||||
.mesh-hopviz-dot:nth-child(6) { animation-delay: 1.25s; }
|
||||
@keyframes mesh-hopviz-blink { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mesh-hopviz-chain > * { animation: none; opacity: 1; }
|
||||
.mesh-hopviz-link::before { animation: none; display: none; }
|
||||
.mesh-hopviz-dot { animation: none; }
|
||||
}
|
||||
.mesh-hopviz-node { display: flex; flex-direction: column; align-items: center; gap: 4px; min-width: 72px; }
|
||||
.mesh-hopviz-icon { font-size: 1.8rem; }
|
||||
.mesh-hopviz-name { font-size: 0.8rem; font-weight: 600; color: #fff; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -604,5 +636,10 @@ select.mesh-bitcoin-input option { background: #1a1a2e; color: rgba(255,255,255,
|
||||
.mesh-hopviz-meta { font-size: 0.75rem; color: rgba(255,255,255,0.5); }
|
||||
.mesh-hopviz-note { flex-basis: 100%; text-align: center; font-size: 0.68rem; color: rgba(255,255,255,0.35); margin: 2px 0 0; }
|
||||
|
||||
/* Per-message "more" button — opens the route/hops modal (same target as the
|
||||
transport pill, but always visible and discoverable). */
|
||||
.mesh-chat-more-btn { background: none; border: none; color: rgba(255,255,255,0.45); font-size: 0.9rem; line-height: 1; padding: 0 4px; cursor: pointer; border-radius: 6px; }
|
||||
.mesh-chat-more-btn:hover { color: #fff; background: rgba(255,255,255,0.1); }
|
||||
|
||||
/* Reaction dropdown inside the message action menu */
|
||||
.mesh-chat-reaction-dropdown { flex-basis: 100%; display: flex; flex-wrap: wrap; gap: 4px; padding-top: 6px; }
|
||||
|
||||
@@ -362,6 +362,19 @@ init()
|
||||
</button>
|
||||
</div>
|
||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||
<!-- v1.7.118-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.7.118-alpha</span>
|
||||
<span class="text-xs text-white/40">July 29, 2026</span>
|
||||
</div>
|
||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||
<p>Fixes mesh radios dropping off on nodes that took the v1.7.117 update. Updates only ever replaced the main program, never the packaged radio helpers — so updated nodes were left running an older radio daemon that didn't understand a new option and quietly gave up, showing "device not connected" with a Connect button that did nothing. The node now checks what its radio daemon supports before using new options, and updates finally carry the radio helpers themselves, so every node gets current radio support with the update instead of only from a fresh install.</p>
|
||||
<p>The in-app "Flash LoRa" flow works on updated nodes. The RNode flashing tool was only ever included on freshly installed nodes; everywhere else flashing failed with a cryptic "No such file or directory". The tool now ships with updates and is included on new install images, and if it's somehow still missing the error says exactly what to do instead.</p>
|
||||
<p>Message notification badges finally remember what you've read. Unread counts were only kept in memory, so every visit re-counted old messages as new — including a phantom badge for chats with nothing new in them. Read-state is now saved on the device, opening a chat marks all its linked conversations read, and history no longer re-badges after a reload.</p>
|
||||
<p>Every mesh message now has a visible "⋯" button that opens the route view: watch the path your message took animate — sender and receiver appear, a pulse travels the link, and each relay hop lights up in order — with signal quality for radio links and delivery status. (Tapping the transport pill still works too.)</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- v1.7.117-alpha -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
|
||||
+17
-23
@@ -1,36 +1,30 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Flash your LoRa radio from inside the app. The Mesh page now has a \"Flash LoRa\" button that opens a guided flow: pick the firmware family (MeshCore, Meshtastic, or Reticulum RNode) and your board, and the node downloads the latest release and flashes it with live progress — no external flasher website, no cables to a computer. The same flow appears when a freshly plugged-in radio is detected, and a long list of flashing pitfalls was fixed along the way: radios no longer boot-loop after a flash, failures show the real error instead of silently bouncing back, wedged flash jobs can't get stuck forever, and board auto-detection no longer misidentifies Heltec boards.",
|
||||
"Every Archipelago node now acts as a Reticulum relay. Nodes forward mesh traffic and re-broadcast peer announcements, so two radios that can't hear each other directly can still discover and message each other through any Archipelago node in between — your nodes become infrastructure for the whole neighbourhood mesh, including non-Archipelago apps like Sideband.",
|
||||
"Reticulum (RNode) radios are now first-class mesh citizens. Radios are reliably detected on node startup (a boot-timing race used to leave them unclaimed), settings changes apply live without a restart, your node's name propagates over the Reticulum network so other apps like Sideband see it properly, and a crashed Reticulum daemon is detected and restarted automatically. Photo and file attachments sent over Reticulum now actually arrive — four separate delivery bugs were found and fixed, verified end-to-end over real radio hardware.",
|
||||
"Messages to contacts that exist on both the internet mesh and a LoRa radio now prefer the radio when it's live, and attachments follow the same path — so co-located nodes talk over the air even when the internet path exists.",
|
||||
"Mesh chat polish: each message in the image viewer shows which transport carried it, a new hop-route view shows the path a message took, reactions moved into a tidy dropdown, and read-tracking now reflects what you've actually seen. The Refresh and Broadcast buttons give real feedback, and the radio-setup modal shows honest probe progress instead of freezing.",
|
||||
"The wallet transactions list works properly on phones now: it scrolls (it silently couldn't on touch screens before), and the All / On-chain / Lightning / Ecash filter tabs stay pinned at the top with a subtle blur while the list scrolls underneath.",
|
||||
"Backend services no longer masquerade as launchable apps. Anything without a real web interface — databases, APIs, background workers, including stacks you deploy by hand for testing — now files under Services with no Launch button. Apps declare their interface in their manifest; for everything else the node checks the port itself to see whether a browser page actually lives there.",
|
||||
"Lightning payments that take a while (slow multi-hop routes) are no longer reported as failed while they're still in flight. The wallet now waits properly, shows an honest \"pending\" state, and reports the true final outcome.",
|
||||
"Server pages feel instant: Server, Federation, Lightning channels, Monitoring, wallet, Cloud, and Credentials screens now render immediately from a shared cache and refresh live in the background (including push updates over the node's websocket), instead of blanking while every panel refetches.",
|
||||
"The node stays responsive under heavy load: the connection handler now sheds excess load instead of stalling everything behind it, and companion-app probes no longer trigger container builds during routine checks."
|
||||
"Fixes mesh radios dropping off on nodes that took the v1.7.117 update. Updates only ever replaced the main program, never the packaged radio helpers — so updated nodes were left running an older radio daemon that didn't understand a new option and quietly gave up, showing \"device not connected\" with a Connect button that did nothing. The node now checks what its radio daemon supports before using new options, and updates finally carry the radio helpers themselves, so every node gets current radio support with the update instead of only from a fresh install.",
|
||||
"The in-app \"Flash LoRa\" flow works on updated nodes. The RNode flashing tool was only ever included on freshly installed nodes; everywhere else flashing failed with a cryptic \"No such file or directory\". The tool now ships with updates and is included on new install images, and if it's somehow still missing the error says exactly what to do instead.",
|
||||
"Message notification badges finally remember what you've read. Unread counts were only kept in memory, so every visit re-counted old messages as new — including a phantom badge for chats with nothing new in them. Read-state is now saved on the device, opening a chat marks all its linked conversations read, and history no longer re-badges after a reload.",
|
||||
"Every mesh message now has a visible \"⋯\" button that opens the route view: watch the path your message took animate — sender and receiver appear, a pulse travels the link, and each relay hop lights up in order — with signal quality for radio links and delivery status. (Tapping the transport pill still works too.)"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.117-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.117-alpha/archipelago",
|
||||
"current_version": "1.7.118-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.118-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.117-alpha",
|
||||
"sha256": "a63bf41ac00e0b77da7bf3796e55a95e2973d4fc335580c5157bd7a506b4a59b",
|
||||
"size_bytes": 53577760
|
||||
"new_version": "1.7.118-alpha",
|
||||
"sha256": "c1a68f72ae832ba2e8668aae4582d4beaf561463a49d19a87881d46a2fb724d0",
|
||||
"size_bytes": 53336720
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.117-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.117-alpha/archipelago-frontend-1.7.117-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.117-alpha.tar.gz",
|
||||
"new_version": "1.7.117-alpha",
|
||||
"sha256": "10d409308736a664ede4415d086e1fd9b9b8c25aed4649c3b89562df97d169a0",
|
||||
"size_bytes": 178025421
|
||||
"current_version": "1.7.118-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.118-alpha/archipelago-frontend-1.7.118-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.118-alpha.tar.gz",
|
||||
"new_version": "1.7.118-alpha",
|
||||
"sha256": "12bb73af26ebd6bdd314e1c8502c414605cde883b9e91a3792cb235b8faf4f25",
|
||||
"size_bytes": 210502532
|
||||
}
|
||||
],
|
||||
"release_date": "2026-07-29",
|
||||
"signature": "d1a00037db19cdadf28950e43037dea4b3316d606001593f59afadf13c8fc43502a1404732eb9701f2f2ad9b1803f023a81bbd30d884432499276aeca0711a09",
|
||||
"signature": "ed18686682ff56613dc7c75ab137b53729d24dfcf7d7298cb4c5f06005c4b57c298b822c8692483aa847496bf09ecdd476f34f30c684cc5b76192ff357ee9609",
|
||||
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
|
||||
"version": "1.7.117-alpha"
|
||||
"version": "1.7.118-alpha"
|
||||
}
|
||||
|
||||
+17
-23
@@ -1,36 +1,30 @@
|
||||
{
|
||||
"changelog": [
|
||||
"Flash your LoRa radio from inside the app. The Mesh page now has a \"Flash LoRa\" button that opens a guided flow: pick the firmware family (MeshCore, Meshtastic, or Reticulum RNode) and your board, and the node downloads the latest release and flashes it with live progress — no external flasher website, no cables to a computer. The same flow appears when a freshly plugged-in radio is detected, and a long list of flashing pitfalls was fixed along the way: radios no longer boot-loop after a flash, failures show the real error instead of silently bouncing back, wedged flash jobs can't get stuck forever, and board auto-detection no longer misidentifies Heltec boards.",
|
||||
"Every Archipelago node now acts as a Reticulum relay. Nodes forward mesh traffic and re-broadcast peer announcements, so two radios that can't hear each other directly can still discover and message each other through any Archipelago node in between — your nodes become infrastructure for the whole neighbourhood mesh, including non-Archipelago apps like Sideband.",
|
||||
"Reticulum (RNode) radios are now first-class mesh citizens. Radios are reliably detected on node startup (a boot-timing race used to leave them unclaimed), settings changes apply live without a restart, your node's name propagates over the Reticulum network so other apps like Sideband see it properly, and a crashed Reticulum daemon is detected and restarted automatically. Photo and file attachments sent over Reticulum now actually arrive — four separate delivery bugs were found and fixed, verified end-to-end over real radio hardware.",
|
||||
"Messages to contacts that exist on both the internet mesh and a LoRa radio now prefer the radio when it's live, and attachments follow the same path — so co-located nodes talk over the air even when the internet path exists.",
|
||||
"Mesh chat polish: each message in the image viewer shows which transport carried it, a new hop-route view shows the path a message took, reactions moved into a tidy dropdown, and read-tracking now reflects what you've actually seen. The Refresh and Broadcast buttons give real feedback, and the radio-setup modal shows honest probe progress instead of freezing.",
|
||||
"The wallet transactions list works properly on phones now: it scrolls (it silently couldn't on touch screens before), and the All / On-chain / Lightning / Ecash filter tabs stay pinned at the top with a subtle blur while the list scrolls underneath.",
|
||||
"Backend services no longer masquerade as launchable apps. Anything without a real web interface — databases, APIs, background workers, including stacks you deploy by hand for testing — now files under Services with no Launch button. Apps declare their interface in their manifest; for everything else the node checks the port itself to see whether a browser page actually lives there.",
|
||||
"Lightning payments that take a while (slow multi-hop routes) are no longer reported as failed while they're still in flight. The wallet now waits properly, shows an honest \"pending\" state, and reports the true final outcome.",
|
||||
"Server pages feel instant: Server, Federation, Lightning channels, Monitoring, wallet, Cloud, and Credentials screens now render immediately from a shared cache and refresh live in the background (including push updates over the node's websocket), instead of blanking while every panel refetches.",
|
||||
"The node stays responsive under heavy load: the connection handler now sheds excess load instead of stalling everything behind it, and companion-app probes no longer trigger container builds during routine checks."
|
||||
"Fixes mesh radios dropping off on nodes that took the v1.7.117 update. Updates only ever replaced the main program, never the packaged radio helpers — so updated nodes were left running an older radio daemon that didn't understand a new option and quietly gave up, showing \"device not connected\" with a Connect button that did nothing. The node now checks what its radio daemon supports before using new options, and updates finally carry the radio helpers themselves, so every node gets current radio support with the update instead of only from a fresh install.",
|
||||
"The in-app \"Flash LoRa\" flow works on updated nodes. The RNode flashing tool was only ever included on freshly installed nodes; everywhere else flashing failed with a cryptic \"No such file or directory\". The tool now ships with updates and is included on new install images, and if it's somehow still missing the error says exactly what to do instead.",
|
||||
"Message notification badges finally remember what you've read. Unread counts were only kept in memory, so every visit re-counted old messages as new — including a phantom badge for chats with nothing new in them. Read-state is now saved on the device, opening a chat marks all its linked conversations read, and history no longer re-badges after a reload.",
|
||||
"Every mesh message now has a visible \"⋯\" button that opens the route view: watch the path your message took animate — sender and receiver appear, a pulse travels the link, and each relay hop lights up in order — with signal quality for radio links and delivery status. (Tapping the transport pill still works too.)"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"current_version": "1.7.117-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.117-alpha/archipelago",
|
||||
"current_version": "1.7.118-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.118-alpha/archipelago",
|
||||
"name": "archipelago",
|
||||
"new_version": "1.7.117-alpha",
|
||||
"sha256": "a63bf41ac00e0b77da7bf3796e55a95e2973d4fc335580c5157bd7a506b4a59b",
|
||||
"size_bytes": 53577760
|
||||
"new_version": "1.7.118-alpha",
|
||||
"sha256": "c1a68f72ae832ba2e8668aae4582d4beaf561463a49d19a87881d46a2fb724d0",
|
||||
"size_bytes": 53336720
|
||||
},
|
||||
{
|
||||
"current_version": "1.7.117-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.117-alpha/archipelago-frontend-1.7.117-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.117-alpha.tar.gz",
|
||||
"new_version": "1.7.117-alpha",
|
||||
"sha256": "10d409308736a664ede4415d086e1fd9b9b8c25aed4649c3b89562df97d169a0",
|
||||
"size_bytes": 178025421
|
||||
"current_version": "1.7.118-alpha",
|
||||
"download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.118-alpha/archipelago-frontend-1.7.118-alpha.tar.gz",
|
||||
"name": "archipelago-frontend-1.7.118-alpha.tar.gz",
|
||||
"new_version": "1.7.118-alpha",
|
||||
"sha256": "12bb73af26ebd6bdd314e1c8502c414605cde883b9e91a3792cb235b8faf4f25",
|
||||
"size_bytes": 210502532
|
||||
}
|
||||
],
|
||||
"release_date": "2026-07-29",
|
||||
"signature": "d1a00037db19cdadf28950e43037dea4b3316d606001593f59afadf13c8fc43502a1404732eb9701f2f2ad9b1803f023a81bbd30d884432499276aeca0711a09",
|
||||
"signature": "ed18686682ff56613dc7c75ab137b53729d24dfcf7d7298cb4c5f06005c4b57c298b822c8692483aa847496bf09ecdd476f34f30c684cc5b76192ff357ee9609",
|
||||
"signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur",
|
||||
"version": "1.7.117-alpha"
|
||||
"version": "1.7.118-alpha"
|
||||
}
|
||||
|
||||
@@ -115,6 +115,23 @@ if [ -z "$FRONTEND_ARCHIVE" ]; then
|
||||
cp "$PROJECT_ROOT/image-recipe/configs/nginx-archipelago.conf" \
|
||||
"$RUNTIME_DIR/image-recipe/configs/nginx-archipelago.conf"
|
||||
fi
|
||||
# Packaged radio tools ride the runtime payload: OTA-only nodes never
|
||||
# get them any other way — the v1.7.117 rollout left fleet nodes with
|
||||
# a stale archy-reticulum-daemon (exits on the new --enable-transport
|
||||
# flag → mesh dead) and no archy-rnodeconf at all (Flash LoRa fails
|
||||
# with "No such file or directory"). bootstrap.rs promotes these to
|
||||
# /usr/local/bin on first startup after the update.
|
||||
for tool in archy-reticulum-daemon archy-rnodeconf; do
|
||||
if [ -f "$PROJECT_ROOT/reticulum-daemon/dist/$tool" ]; then
|
||||
mkdir -p "$RUNTIME_DIR/radio-tools"
|
||||
echo " Including radio tool $tool"
|
||||
cp "$PROJECT_ROOT/reticulum-daemon/dist/$tool" "$RUNTIME_DIR/radio-tools/$tool"
|
||||
else
|
||||
echo " ERROR: reticulum-daemon/dist/$tool missing — run reticulum-daemon/build.sh first" >&2
|
||||
rm -rf "$STAGING_DIR"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
rm -rf "$RUNTIME_DIR/scripts/resilience/reports"
|
||||
find "$RUNTIME_DIR" -type d -name '__pycache__' -prune -exec rm -rf {} +
|
||||
find "$RUNTIME_DIR" -type f \( -name '*.bak' -o -name '*.bak-*' -o -name '._*' -o -name '*.log' -o -name '*.pyc' \) -delete
|
||||
|
||||
@@ -167,6 +167,16 @@ if ! grep -rqo "${VERSION}" "$PROJECT_ROOT"/web/dist/neode-ui/assets/*.js; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[4b/8] Building packaged radio tools (archy-reticulum-daemon, archy-rnodeconf)..."
|
||||
# These ride the frontend tarball's runtime payload (radio-tools/) and are
|
||||
# promoted to /usr/local/bin by bootstrap.rs — the ONLY path that updates them
|
||||
# on OTA-only nodes. Stale-dist releases re-broke fleet mesh once (v1.7.117),
|
||||
# so always rebuild here; the manifest script hard-fails if they're missing.
|
||||
(cd "$PROJECT_ROOT/reticulum-daemon" && ./build.sh) || {
|
||||
echo "Error: reticulum-daemon/build.sh failed — radio tools are release-critical" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "[5/8] Validating curated changelog..."
|
||||
|
||||
CHANGELOG_FILE="$PROJECT_ROOT/CHANGELOG.md"
|
||||
|
||||
Reference in New Issue
Block a user