Skip to content

v1.6.0 - #19

Merged
zevaryx merged 61 commits into
mainfrom
dev
Oct 8, 2026
Merged

zevaryx merged 61 commits into
mainfrom
dev

Conversation

@zevaryx

@zevaryx zevaryx commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Changes since v1.5.1.

Added

  • Messaging
    • Search messages, in one conversation or all of them (/).
    • Search channel history: what was said in every RRC room and whisper conversation, or the one open (/ in Channels).
    • Pin conversations (*) and mark all read (R).
    • Forward a message (f), and export a conversation as text (E).
    • Read archived messages (H).
    • Message requests: Unknown senders can be set to show, requests or ignore.
    • Sideband/MeshChat icons are shown, and you can set your own.
    • Pictures are shrunk before sending, set by Send pictures at.
    • Voice messages can be recorded in the web UI and are sent as Codec2.
    • Answer Sideband's ping, echo and signal-report commands (Answer commands, off by default).
  • Locations
    • Share a location (L / 📍) in Sideband's telemetry format, once or live (as Columba does: updates for a while, then a message saying it stopped).
    • Answer location requests (Location requests, off by default).
    • A map of shared locations in both UIs (M). Map tiles are fetched and cached by rettui.
    • An offline map: Offline map names an MBTiles file whose tiles the web map shows first, with no internet at all (enlarged past the file's closest zoom).
  • Network
    • Find or forget the path to any destination: P / D, or the Path dialog in the web UI (#10).
    • Probe any destination (T).
    • New rettui path and rettui probe commands, like rnpath and rnprobe.
    • Sort the Network list (s), or keep it to one interface (i).
    • An announce viewer (a in Network): every announce as it's heard, of every kind (RRC hubs, calls and others too), with when, how far and through which interface.
    • Pings report RSSI/SNR when answered over an RNode, and so do messages that came in one packet straight from the sender (📶 beside them).
    • Status shows rnstatus-style interface details, and a graph of each interface's traffic over the last ten minutes.
  • Browser
    • Search nodes and saved pages (/).
    • Save a node without opening it (s, or ☆ in the web UI).
    • Find in the page (f).
  • Web UI
    • HTTPS with --https (rettui's own certificate authority), or --tls-cert / --tls-key for your own certificate.
    • Works behind a reverse proxy, including under a sub-path.
    • Drafts survive reloads.
    • Light theme.
    • Keyboard shortcuts, with ? to list them.
    • Sign out other browsers.
  • Terminal UI
    • Colour themes (dark, light, basic).
    • The footer shows the shortest hints that fit; ? (or F1 while typing) lists every key.
  • Installing and updating
    • Update checks: once a day, rettui can ask GitHub whether a newer release is out, and marks the version (↑) and Status when one is. Off by default; the getting-started guide recommends turning it on (Check for updates).
    • Release binaries can install a newer release in their own place when asked (U in Status, Install in the web UI, or rettui update), checked against the release's SHA256SUMS and run once before they replace the old one. Builds from source, containers and package managers' are told how to update instead.
    • Packages: a Homebrew tap (brew install zevaryx/rettui/rettui) and the AUR (rettui-bin), published with each release, and Debian packages (x86-64 and ARM64) attached to it.
  • Settings and CLI
    • Quiet hours: no notifications between two times of day, except (unless turned off) from trusted contacts.
    • Back up everything to one file and restore it: B in Status, rettui backup / rettui restore, or Download a backup in the web UI (without the identity).
    • 12-hour clock and date order.
    • Log level as a setting.
    • rettui send accepts lxma:// links.

Changed

  • Path requests are retried at 0, 15 and 52 s. A stale path is replaced and the Link is tried again before giving up (#10). A page loading, an RRC hub connecting and rettui fetch say which request is out.
  • Pictures are sent at 1024 px by default. Set Send pictures at to original for the old behaviour.
  • Ignore unknown senders is replaced by Unknown senders; existing settings carry over.
  • Updated to rsReticulum 1.3.0 and rsLXMF main. rsReticulum follows the rettui branch of zevaryx/rsReticulum until ratspeak/rsReticulum#26 is merged upstream.

Fixed

  • Background notifications, map tiles and update checks failed behind a proxy that inspects HTTPS: they now trust the computer's own certificates as well as the bundled ones.
  • Destinations whose first path request went unanswered could only be reached after their next announce (#10).
  • The P/T hints were missing while a Network search was active.

Docs and development

  • A documentation site, zevaryx.github.io/rettui, built from wiki/ with Zensical.
  • Instructions for running rettui as a systemd service.
  • The code is formatted with rustfmt, and CI checks it.
  • A release's notes are its section of this changelog; tagging fails without one.

Known limitations

  • Messages received over a Link (most direct messages) don't show RSSI/SNR: rsReticulum doesn't say how a Link's packets were heard.
  • LXST voice calls aren't supported because of LXST's license (CC BY-NC-ND 4.0).

claude and others added 30 commits October 6, 2026 20:31
/ in the Browser tab opens a search line under the Saved and Nodes
tabs: as you type, both lists narrow to what matches by name or
address, as the Network tab's search does (every word must match;
<hash> and hash:/page pasted work). Matches are highlighted; a node
or page found by its address shows the address. The tabs count the
matches ("Nodes 4/120"), Enter keeps the search, Esc clears it, Ctrl-V
pastes, and a click on the line starts one.

Removing a saved page while searching removes the one shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A search box under the Browser's Saved and Nodes tabs narrows both
lists by name or address, with the Network search's rules. Nodes are
searched by rettui, so it finds any node heard, not only the newest
200 listed; saved pages narrow as you type. Matches are highlighted,
a saved page found by its address shows it, and the tabs count the
matches ("Nodes 1/252"). / focuses it on the Browser tab, Esc clears
it, and the list's note no longer sends you to the Network tab.

/peers also says how many of the kind were heard before the search
narrowed them ("heard").

Checked in Chromium with 252 nodes and two saved pages, at desktop
and phone widths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
In the TUI, s with the Nodes list in hand saves the selected node's
home page; with the page in hand it saves the page, as before. In the
web UI, each node in the list has a star: ☆ saves its home page, ★
removes it. Either way it's the same bookmark as saving the home page
once open, so it's never saved twice, and a node whose home page is
saved is starred in both lists.

Checked in Chromium: a node found by a search saved and removed from
its star, without opening it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
NomadNet-Browsing says how to search the Saved and Nodes lists and
save a node without opening it, in both UIs; Using-the-TUI lists /,
s and Esc in the Browser pane, and Web-UI that / searches the Browser
and its search finds any node, beyond the 200 listed.

The Browser pane's footer has / search, and s save node in the Nodes
list (x remove saved in Saved); while a search is in effect, edit or
clear it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Before browsing a node, sending, syncing or joining an RRC hub, rettui
sent one path request and gave up after 15 s (rsReticulum's transport
ends every wait there, though rettui asked for 30), so a destination
whose path request went unanswered could only be reached after its next
announce (issue #10).

Path requests now go out at 0, 15 and 52 s, with a last wait of 15 s.
The second covers a first lost on the way. Python Reticulum (1.5.x and
current) folds further requests for a destination into one it's still
working on for 45 s (PATH_REQUEST_GATE_TIMEOUT, culled every 5 s), on
the destination and on any hub between, so the third goes after that,
when a node that heard the first looks again. Between requests, a path
that comes with an announce is taken.

A Link that isn't answered may have gone over a stale path, or one
answered from an out-of-date cache: the path is dropped, a fresh one
found, and the Link tried once more. This covers page loads, direct
and propagated sends, syncs and RRC hubs (not ping, which times one
Link).

The terminal browser's load line, the RRC hub status and `rettui fetch`
say which path request is out.

Checked against a Python RNS 1.5.5 node over loopback TCP: one that
ignored the first path request served the page after the third (52 s);
one that ignored the first Link request served it after the path was
replaced (12 s). Both failed before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
As rnpath does (issue #10). For any address (a peer, a node, a hub or
one typed in), rettui shows the path it knows: hops, the transport node
it goes through, the interface and how long it's kept. If none is
known, it asks for one, at 0, 15 and 52 s as when connecting, and says
which request is out. Forgetting a path that has gone stale makes the
next use ask for a fresh one.

- Terminal UI, Network tab: P finds a path (to the selected row, or any
  address typed in its place), D forgets the selected row's path. The
  answer shows as a notice and in the log.
- Web UI, Network page: a Path button on each row, and "Find a path…"
  for any address, open a dialog with the path, how finding it is
  going, and Find path / Forget path.

Checked against a Python RNS node over loopback TCP in both UIs: found
("heard directly on Python side, kept for 6 days"), forgotten, found
again; an address nobody has shows "path request 2 of 3" while it's
looked for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A probe times an answer from a destination, finding a path first the
same way a page load does. LXMF addresses and transport probe
destinations prove packets, so they get a probe packet, as rnprobe
sends. NomadNet nodes, propagation nodes and RRC hubs don't, and
rnprobe to one always times out, so they get a timed Link instead,
and the answer says so.

T probes the selected Network row in the TUI. The web Path dialog
has a Probe button, and each Network row a Path button; the actions
column is wider and wraps.

Checked against a Python node and LXMF peer: "a Link opened in 2 ms,
heard directly (NomadNet node)", "answered in 1 ms, heard directly
(LXMF address)", and an unknown address shows how finding a path goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
rettui path <address> finds the path to any destination and prints
it, asking for one if none is known (three requests over a minute,
as the UIs do). -d forgets it, so the next use asks for a fresh one;
-t lists every path known. With rettui or rnsd running as the shared
instance these are its paths, read and dropped over its RPC, as
rnpath does; else, the paths the last run with this Reticulum config
saved, which a drop changes for the next run.

rettui probe <address> times an answer: a probe packet for LXMF
addresses, a Link for nodes, propagation nodes and RRC hubs, which
don't prove packets. --name takes a destination's full name for kinds
rettui can't tell, which are sent a probe packet.

Checked against a Python node and LXMF peer, standalone and as a
client of rettui --web as the shared instance: paths found, listed,
dropped (and gone from the shared instance's table), both kinds
probed, and an unknown address reported after 67 s, with each
request as it goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Network says how paths are found (three requests over a minute, and
why the third waits for Reticulum's 45 s gate), how stale ones are
replaced, finding and forgetting one in both UIs, how probes are
answered by each kind, and the commands. Running lists rettui path
and rettui probe, Using-the-TUI the P, D and T keys, and
NomadNet-Browsing what to do about a node that won't answer.

The Network tab's footer shows P (path) and T (probe).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Once a search is typed, the Network footer is another one, and it
didn't have P (path) and T (probe), though both work on the rows
found. They come right after the search's own keys, before the longer
hints, as the footer is cut off at the terminal's edge: a 120-column
terminal shows them. While typing, they go into the search box, so
that footer stays without them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Each mode's keys are now one list, most used first, with the shortest
labels that still say what they do ("p sync via", "P path", "D forget
path"). The footer shows as many as fit whole, nano style ("P path"),
so none is cut off mid-word, and "? keys" stays at its right edge.
The keys that work in every tab (1-7, A, S, ^L, q) leave the tabs'
footers for that list.

? opens a list of every key of what's on screen, in columns, with
those that work in every tab; any key or a click closes it, and does
nothing else. While typing, ? is typed, so the footer says "F1 keys"
and F1 opens the list, with the keys that work while typing.

Network's footer now shows P path and T probe at 80 columns, searched
or not. Checked in tmux: ? and F1, the list for Network, Messages and
the Network search, and typing ? into the search.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Brings in the path tools and the new footer (#13). The Browser
search's hints move into the keys table: / search and s save in the
Nodes list, x remove in Saved, edit or clear while a search is in
effect, the search box's own keys while typing (F1 for the list), and
/ search from the page and the source view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
zevaryx.github.io/rettui is built from the pages in wiki/, which stay
the one copy (the GitHub wiki is published from them too).
docs/build.py writes them as the site wants them: links between pages
point at their files, Home is the front page, each page is titled
with its sidebar name, GitHub's [!WARNING] notes become admonitions,
and the navigation is wiki/_Sidebar.md. A link to a page or heading
that doesn't exist, or a page left out of the sidebar, stops the
build, and Zensical builds in strict mode.

The Docs workflow builds it for pull requests that change it and
publishes it to GitHub Pages from main. The README points at it, and
Development describes the workflow. Fixes a link to a heading that
NomadNet-Browsing doesn't have, which the check found.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
rsReticulum moves to 1.3.0 (upstream main) with ratspeak/rsReticulum#26
merged in, which node hosting needs (file replies, their metadata and
the requester's identity). Until that pull request is merged upstream,
the submodule follows the rettui branch of zevaryx/rsReticulum
(6825661). From 1.3: path requests a busy or slow interface couldn't
take are kept and sent once it can, a failed Link's route is dropped
before recovery, Resource transfers have bounded waits, a shared
instance client attaches without a startup race, and on Windows a
process rettui starts no longer keeps its listeners open. The one
breaking change (recursive discovery internals) isn't used here.

rsLXMF moves to main, which needs rsReticulum 1.3: delivery and lxmd
changes rettui doesn't use, and a propagation node fix for messages
stored without a stamp (rettui stores them stamped).

No code changes. Checked against Python RNS 1.5.5 and LXMF 1.2.0: path
retries (52 s with the first request dropped, 12 s with the first Link
dropped), page fetch, ping, probe, direct and opportunistic messages
both ways (with ratchets), a propagated message and sync, the CLI as a
shared instance's client, and hosting a node (a page, a file with its
name, and an executable page given the visitor's identity). rsNomad's
own 104 tests pass against rsReticulum 1.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
All the web UI's addresses are relative now (the page, its script,
style, fonts, icons, manifest, service worker, API and live
connection, and the login form and redirect), so a proxy can serve it
at a site's root or under a path such as /rettui/ with nothing to set.
The live connection asks nginx not to buffer it, and an upload a
proxy turns away as too large says so (nginx's 1 MB default).

Checked in Chromium through nginx 1.24 with only proxy_pass, under
/rettui/: login, the API, fonts, a live update and the service
worker's scope; and directly, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Off unless asked for at startup, so existing installs stay plain HTTP.
--https serves the web UI over HTTPS with rettui's own certificate: a
small certificate authority made once (web-tls/ in the data
directory), and a certificate it signs for this computer's names and
addresses, made again when they change or a month before it ends (397
days, as Apple's devices accept). Devices install the authority from
/rettui-ca.crt, or Download under Status, which explains how on
Android, an iPhone and a computer. --tls-cert and --tls-key use a
certificate of your own (tailscale cert, mkcert, Let's Encrypt).

With HTTPS on, a plain http:// request to the port is redirected to
https://, and the login cookie is Secure. Handshakes run on their own
(a slow one doesn't hold up others) on rustls with ring, as ureq
already builds it.

The Status page's Identity panel scrolls on its own when its rows are
taller than its share, rather than running over Settings.

Checked: curl verifies the certificate for localhost and 127.0.0.1
with only the authority; Chromium trusting it (its NSS database) sees
a secure context, a Secure cookie, a registered service worker and the
notifications API; the download; your own certificate files; and the
flags' errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
In the TUI, `/` in Messages opens a search over every conversation: every
word typed must be found, in any order, in a message's text, title,
attachments' names or notes. Results are newest first (200 at most), with
who, when and the part that matched, highlighted. Tab keeps it to the open
conversation, and Enter opens the conversation scrolled to the message,
picked.

In the web UI, a Search messages box above the conversations (or `/`)
lists the same, from /api/search?q=...&in=<address>. "In <name>" keeps it
to the open conversation, and a result further back than what's loaded
loads the rest before jumping to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A photo straight from a phone is megabytes: minutes over a radio link, and
more than propagation nodes usually take (256 KB). As Sideband, Columba
and MeshChat do, pictures are now made smaller first, by the new "Send
pictures at" setting (picture_size): small (480 px on the longest side,
for LoRa), medium (1024 px, the default), large (2048 px) or original.

A picture is turned the way its camera noted (EXIF orientation) and
written again as a JPEG, or a PNG if it's partly see-through, which leaves
out its metadata, such as where a photo was taken. GIFs go as they are,
and so does a picture that wouldn't get smaller. The smaller copy is saved
in uploads/, so the picture on your computer isn't changed, and the copy
goes when its message is deleted; an uploaded original is replaced.

The web UI shrinks uploads before taking the app's lock (a 12 MP photo
takes about a quarter of a second), says "sent smaller" on the picture
while writing, and no longer warns of a picture's size before it shrinks.
The TUI shows "(smaller)" beside the picture, and a message it can't send
gives back the files picked, not the copies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Their icons: LXMF's icon appearance field ([name, foreground, background],
a Material Design Icon's name and two 3-byte colours, as Sideband and
MeshChat send it) is read, and the newest each person sent is kept with
their contact. The web UI shows it beside their name in the conversation
list and above the conversation (once anyone has one, the rest get their
first letter); the TUI's contact card names it in its colours. A message
carrying only an icon updates it without showing as a message.

Yours: new Icon, Icon colour and Icon background settings. Once an icon is
set it goes with every message (not paper ones); empty, the default,
sends none, so nothing changes for existing installs. The web UI picks it
from the icons matching what's typed (GET /api/icons?q=), shows colours
with colour inputs (a new colour kind of setting), and shows your icon
beside your name.

The names map to their characters in Nerd Font's icons (src/icons/mdi.txt:
the 6,896 MDI 7.4.47 icons the bundled font has), so the server sends the
character and the browser needs no table. Checked against Python LXMF
both ways.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Drafts in the web UI lived only in the page, and phones reload pages
they've put in the background, so a half-written message could vanish.
The text of each conversation's and room's draft, and the message it
replies to, are now kept in the browser's localStorage: as typed (a
moment after), on switching, on sending (a sent draft goes), and when the
page is hidden or left. They come back where they were written after a
reload or a restart of rettui. Attached files stay in the page only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Messages past "Messages kept" move to archive/, which neither UI could
show. Now they open from their conversation, only to read:

- TUI: H in Messages opens them over the tab, newest at the bottom; the
  arrows, PgUp/PgDn, Home/End and the mouse wheel scroll, and Esc (or a
  click outside) closes. The note at the top of a conversation says so.
- Web UI: the note at the top of a conversation is now a "Show N archived
  messages" button, which reads them from GET
  /api/conversations/<address>/archive (away from the app) and shows them
  above the rest, without reply, react or delete buttons. Their files and
  pictures still open: the attachment route looks in the archive for a
  message it doesn't find among those kept.

store::read_archive reads a conversation's messages from every month,
oldest first, parsing only the lines that name it.

Also fixes the conversation header in the web UI reading "null" before
the name of someone with no icon (from the icons commit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
"Ignore unknown senders" was all or nothing: messages from anyone who
isn't a contact were shown like anyone's, or dropped. It's now "Unknown
senders" (unknown_senders): show (the default, as before), ignore (as the
toggle did; a settings file with it on reads as this), or requests.

With requests, their messages are kept aside, as Signal does: listed after
the rest (under a "Message requests" heading in the web UI, marked
"request" in the TUI), with no notification and not counted as unread.
Trusting them, leaving them as they are or replying makes it a
conversation like any other; blocking or deleting gets rid of it (the web
UI's request banner has Delete beside Block).

App::conversation_order and App::unread_messages replace the store's
order and the summed unread counts in both UIs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Pings now open their Link with track_phy_stats, and say how well the
answer was heard when it came in over a radio that reports it (an RNode):
"answered in 840 ms, 2 hops away, heard at RSSI -91 dBm, SNR 4.2 dB, link
quality 87%", as rnprobe says it, in both UIs and `rettui ping`.

The Status tab's interfaces show what rnstatus does of each: its rate,
MTU, mode if not full, clients for a server, and announces queued or held
and packets dropped when there are any (from Reticulum's interface
stats).

Received messages still carry no readings: rsReticulum's destination
runtime hands packets and Link data to the application without the
interface's RSSI/SNR (only an outbound Link session keeps them), so
showing them, as Sideband does, needs a change there first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A pinned conversation stays at the top of the list (newest pinned first;
message requests still come last): `*` in the TUI's Messages tab, or the
📌 button above a conversation in the web UI (POST
/api/conversations/<address>/pin). Pinned ones are marked in both lists.

`R` in the TUI, or "✓ All read" above the web UI's conversations while
anything is unread (POST /api/conversations/read-all), marks every
conversation read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Forward: a message's text and files go to another conversation, or to an
address typed in full, as a new message, by how messages to them go. Its
files are copied into uploads/, so deleting either message leaves the
other's. In the TUI, `f` on a picked message opens a list to pick from
(type part of a name or address; Enter, or a click, forwards); in the web
UI, "Forward…" in a message's ⋯ menu opens the same as a dialog (POST
/api/conversations/<address>/forward).

Export: a conversation, archived messages and all, as text: who wrote
what and when, with file names, locations, notes and reactions. `E` in
the TUI's Messages tab writes it to downloads/exports/; "Export as text"
in the web UI's Contact dialog downloads it (GET
/api/conversations/<address>/export).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
…ma:// links

"Sign out the others" (Status, Browsers) makes a new login token: every
other browser, and scripts using the old token, must log in again with
the new link, printed where rettui runs and saved in web_token. The
browser that asked keeps a cookie with the new token. Live-update streams
opened before it end at once (they'd otherwise go on carrying
notifications), and push subscriptions are dropped (the browser that
asked subscribes again). POST /api/sign-out-others.

`rettui send` takes an lxma:// link as well as an address, keeping the
key it carries so the message can go before they've been heard
announcing (as `rettui ping` already did).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
The Network list was always last heard first. `s` in the TUI, or the
order menu in the web UI (GET /api/peers?sort=), now sorts it by name or
nearest first too.

Each row says which interface its path goes through, when one's known:
the network task reads the path table every 30 seconds and passes on the
interface for each destination. `i` in the TUI, or the interface menu in
the web UI (?via=), keeps the list to those through one interface, such
as what's heard over a LoRa radio rather than the internet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
"Log level" (Status, log_level): how much goes to rettui.log, from error
to trace, warn by default as before. It applies at once, through a
reloadable filter on the log file's layer. RETTUI_LOG, if set when rettui
starts, still decides instead (and changing the setting says so).

The Running page gets "Running as a service": a systemd user unit (with
lingering, and where the login link goes), and a hardened system unit
for a machine of its own (its own user, the dialout group for an RNode,
its Reticulum config kept writable under /var/lib/rettui, or rnsd's with
ReadWritePaths), with notes on listening, logs and stopping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
…ht web theme

Times and dates: new Clock (24-hour, or 12-hour as "2:05 PM") and Dates
(month-day "Oct 07", day-month "07 Oct", or year-month-day "2025-10-07")
settings, used everywhere times show: both UIs' lists and messages, the
archive, the log on screen, RRC lines, sync times, exported conversations
and `rettui listen`. A new clock module formats them; the web UI gets the
two settings in /api/state and formats the same way.

Terminal colours (tui_theme): dark (as before), light (a darker accent
and a light selection, for light terminals) or basic (16 colours, for
terminals without full colour). The UI's colour constants became
functions of the theme, so a change shows on the next frame.

The web UI gets a light theme, picked per browser in Status (Theme: dark,
light, or as the device is set, following it when it changes) and kept in
localStorage. The stylesheet's remaining fixed colours became variables,
coloured backgrounds keep their bright values under dark text, room nicks
get darker colours to read on white, and the wordmark is inverted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
claude added 27 commits October 7, 2026 12:23
LXST, which Sideband's calls use, is published under CC BY-NC-ND 4.0:
adaptations of it can't be shared, so it can't be ported into an AGPL
program, and there's no separate protocol description to implement it
from. Said where voice messages are, which work with the same clients.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
rustfmt.toml keeps the code as wide as it was written: lines up to 140
columns, with calls, struct literals, chains and short blocks on one line
while they fit (use_small_heuristics = "Max"); of the widths tried, it
changes the least. Formatting only: nothing else changes. The libraries
in deps/ aren't touched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
CI runs cargo fmt --check on pull requests (it needs no submodules: deps/
isn't formatted). .git-blame-ignore-revs names the formatting commit, so
git blame (GitHub's, or with blame.ignoreRevsFile set) shows who last
changed a line's meaning. Development says to run cargo fmt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Clippy 1.99's chunks_exact_to_as_chunks lint (an error with -D warnings
in CI) flags chunks_exact(2) with a constant size. as_chunks::<2> gives
the pairs as arrays; a trailing odd byte is left out, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
CHANGELOG.md says what's new, changed and fixed in v1.6.0 since v1.5.1:
the quality-of-life changes, path tools, Browser search, the keys list,
the documentation site and rsReticulum 1.3. Earlier releases are on the
releases page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
The settings applied to the running app only those named in
update_settings; anything else was saved to settings.json and waited
for the next start, though these three say they apply at once. A setting
that applies now, with nothing more to do than take the new value, is
now taken whatever its name, so one added later can't be missed.

A test changes every setting that applies now (but those that change
every test's theme, clock or log level) and checks the running app has
it. It caught these three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Once a day, rettui asks GitHub's API for the newest release (drafts and
prereleases left out), in a thread of its own, and compares versions
(a release is newer than its own prereleases). When one is newer:
the version beside the name is marked ↑ in yellow in both UIs and opens
that release's page; Status says which, with a link to what's new; and
the log says so, once a run. Nothing is downloaded or installed.

What it found is kept in update-check.json, so a restart doesn't ask
again and an update found still shows offline. If GitHub can't be
reached it tries again an hour on, only in the debug log. It's one
HTTPS request, through the environment's proxy, with rettui's name and
version as its user agent; the release page linked is always GitHub's.

New setting, Check for updates (update_check), on by default; off, it
never asks and shows nothing. Tests never go online: they give the
check its answer.

The web UI's Status page draws again once a refetched status is in, so
it no longer shows the one before when its own update finishes first.

Installing gets "Updating" (what's shown, how to update each way, and
what the check sends), Data-and-Storage the file, and the changelog a
line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
…them

Check for updates is now off unless turned on: the check tells GitHub the
IP address it comes from, which someone using Reticulum to stay off the
internet (over Tor or I2P) wouldn't want sent without asking.

The getting-started guide, in both UIs, has "Check for updates once a
day (recommended)" after picking a propagation node, not ticked to start
with, saying what it does and that GitHub sees your IP address. Apply
saves it as chosen; opened again, it's as set. It stays in Status too.

Installing, Running and the changelog say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
When an update check has found a newer release, a binary from the
releases page can put it in its own place: U in the TUI's Status (asking
first), Install beside the update in the web UI's Status (confirming
first), or `rettui update` (which checks first, whatever Check for
updates says, and asks unless --yes).

It downloads the release's archive for this system (the Build workflow's
names: .tar.gz, or .zip for Windows) with its SHA256SUMS from GitHub,
checks one against the other, unpacks rettui, and runs it once with
--version beside the old one; only if it says it's the new version does
it replace the old one (by a rename; on Windows the running program is
moved aside, and removed at the next start). rettui carries on as it was
until it's started again, and says so. In a thread of its own; the
checksums catch a download gone wrong, not a release that isn't GitHub's.

Only release builds do this: the Build workflow marks its binaries with
their target (RETTUI_RELEASE_TARGET). A build from source, a container's
(the image sets RETTUI_CONTAINER; /.dockerenv and /run/.containerenv
count too), one a package manager installed (/usr, Homebrew's Cellar,
Nix, snaps), or one in a folder that can't be written to says how to
update instead.

src/update.rs becomes src/update/ with install.rs. New dependencies:
tar, and zip (flate2 only). An online test (ignored unless asked)
downloads, checks and runs the real v1.5.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
The program put in place took the old one's permissions as they were;
one that had lost its execute bits (or a test's stand-in) left the new
one unrunnable, and the check that runs it refused it. It now keeps the
old one's permissions and is runnable by whoever can read it.

Found by the online test, through a proxy that inspects HTTPS (with the
system certificates trusted): the real v1.5.1 downloaded, matched its
SHA256SUMS, unpacked, ran ("rettui 1.5.1") and was put in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Web Push, map tiles and update checks trusted only the root certificates
ureq bundles, so behind a proxy that inspects HTTPS with an authority of
its own (an office's, antivirus) they all failed, quietly. They now
share one TLS configuration that trusts the bundled roots and the
system's (its store, or SSL_CERT_FILE / SSL_CERT_DIR), as browsers do;
a system with no store at all still has the bundled ones. ureq's own
native-certs feature would have replaced the bundled roots instead.

Checked through such a proxy: the update check, which failed with
UnknownIssuer, now finds the latest release, and map tiles still load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
The Release workflow published GitHub's generated notes, so the
changelog could fall behind unnoticed. Now .github/scripts/release-notes.sh
prints the tag's section of CHANGELOG.md (`## v1.7.0`, without the
heading; a prerelease takes its own section, else its release's), the
tag check fails before anything is built if there isn't one, and the
release's notes are that section and the Docker image's name.

Development says to add the section before tagging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
`/` in the Channels tab (the web UI: a search box above the hubs, or `/`)
finds what was said in every hub's rooms and whisper conversations, as
rettui keeps them: every word, in any order, in the line or the name of
who said it, newest first (200 at most), as the message search has it.
Tab (In #room) keeps it to the one open. Joins, leaves and errors aren't
searched.

Opening one shows its room at that line: the TUI scrolls it to about the
middle and marks it until another room is picked; the web UI loads the
whole room if the line is further back than what shows, scrolls to it
and flashes it (GET /api/channels/search?q=&hub=&room=).

The search box over the tab is now one drawer for both searches, and
the snippet around a match works on any texts. Also puts the message
search's doc comment back on its function (the map's view had come
between them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
New Quiet hours setting (quiet_hours): two times of day, by this
computer's clock (22:00-07:00, 22-7, 9:30 to 12; past midnight is fine,
kept as 22:00-07:00). Between them, notifications are held back
wherever they'd show (desktop, browser, Web Push); messages still arrive
and count as unread. Trusted break quiet hours (quiet_hours_trusted, on
by default) lets trusted contacts' messages through anyway.

The check is where every notification is queued, so it covers both UIs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A live share sends location updates to someone for 15 minutes, an hour,
eight hours or until stopped, then a message saying it stopped
(FIELD_CUSTOM_META {"cease": true}, as Columba sends it and rettui
already reads). Each update replaces the one before it in the
conversation, so a live share is one message there, and the
conversation's header says it's live, until when (the web UI: with
Stop).

- Web UI: "Share live" in the share dialog shares where the device is:
  while any share is the device's, the page watches its position and
  posts it (POST /api/live/position, at most every 30 s); rettui sends
  at most one update a minute, and none from a position older than five
  minutes. A page that can't say where the device is shares this
  station's Location instead. POST /api/conversations/<address>/live
  and .../live/stop.
- TUI: `L` then `live 15m`, `live 1h`, `live 8h` or `live on` shares this
  station's Location, every five minutes; `L` again stops it.

Shares last while rettui runs. Checked from Chromium (with a simulated
device location) to Python LXMF: the location arrived, then the cease.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A backup is a .tar.gz of what's yours in the data directory: settings,
the store and its archive, peers, known identities, tickets, RRC history,
the node's pages and (unless left out) the identity; attachments only
when asked. Caches, logs, ratchets and the web UI's secrets are made
again, so they aren't kept.

- `rettui backup [FILE] [--with-files] [--without-identity]` and
  `rettui restore FILE [--force]`, run before anything is loaded (or, in
  a new data directory, made).
- `B` in the TUI's Status tab backs up, with the identity, on the saving
  thread once what's waiting is written.
- The web UI's Status page downloads one, never with the identity.

Backups are written to a new file only their owner can read. Restoring
takes only rettui's own files, skips links, never writes outside the data
directory, and leaves a data directory with an identity or messages alone
unless --force, which moves what it replaces aside first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
`a` in the Network tab (or Announces in the web UI) shows every announce
in place of the list, as it arrives, of every kind: LXMF peers, NomadNet
and propagation nodes, RRC hubs, LXST calls and whatever else announces,
named by the name hash it announced under. Names are read as each known
kind sends them; for others, text (or a name in msgpack) when there is
one. Each row says when it was heard, how far, and through which
interface.

It follows the newest; moving up holds it, and new ones (and old ones
going) don't move the selection. Filter by kind, search and keep to an
interface as in the list; Enter opens what rettui can, and P, T, y and
D work as there. The newest 1000 since rettui started are kept; the web
UI fetches those after the last it has, every 2 seconds while shown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Each interface keeps its rates in and out at every stats update (every
five seconds) for the last ten minutes. The Status tab draws them under
it, against its busiest moment, with how fast it's moving now: bars in
the terminal UI, an SVG line each way in the web UI. An interface that's
been quiet throughout has none, and one gone is forgotten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
*Offline map* (map_tiles_file) names an MBTiles file: map tiles in one
SQLite file, as MOBAC, QGIS or TileMill make them. The web map takes its
tiles from it first, with no internet at all; any it hasn't come from Map
tiles, if that's set, and failing that (or zoomed in past the file's
closest level) the part of the closest tile it has further out, enlarged,
up to six levels. Its attribution is credited in the corner.

The file is opened read only, and only pictures (png, jpg, webp) are
shown: one of vector tiles, or that isn't MBTiles, is refused when it's
chosen. It names a file on this computer, so the web UI can't change it,
as with the node's folder. SQLite comes built in (rusqlite, bundled).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A message that came in one packet straight from the sender (sent
opportunistically) shows how it was heard, when the interface reports
it (an RNode): RSSI, SNR and link quality, beside it in both UIs (📶),
and kept with it. Reticulum keeps those readings by packet hash, as
Python's Transport.get_packet_rssi reads them; rettui hashes the packet
with rsReticulum's own rns-wire and asks the transport (through a shared
instance too). Nothing in deps/ changes.

Messages over a Link (most direct ones) still don't show it: rsReticulum
doesn't say how a Link's packets were heard. The changelog says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
…lease

Each release now also carries:

- Debian packages of the Linux binaries, rettui_<version>_amd64.deb and
  _arm64.deb (rettui in /usr/bin, depending on libc6 >= 2.35 and
  libgcc-s1; a prerelease's version sorts before its release's), in
  SHA256SUMS too;
- for a release, a Homebrew formula (rettui.rb, the ready-built macOS
  and Linux binaries) and the AUR's rettui-bin PKGBUILD (and .SRCINFO),
  written from SHA256SUMS, to publish to a tap and the AUR, which need
  accounts of their own.

The scripts are in .github/scripts; Development says how to publish,
and Installing how to install each. Installed this way, rettui leaves
updating to the package manager, as it already did for /usr/bin and
Homebrew's Cellar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Checking an MBTiles file (choosing it as the offline map, or the web UI
being refused it) opened it into the cache tile serving keeps open, so
it stayed open until another file was. On Windows a file open can't be
deleted or replaced: the settings test's cleanup failed there, and a
user couldn't swap the file while rettui ran.

Checking now opens the file only for the check. Changing or clearing
the setting closes the file kept open for tiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Checked against every commit on dev since main (v1.5.1): what was
missing was the pull requests (#14, #15, #17 and #18 join #12, #13 and
#16) and that a page, an RRC hub and `rettui fetch` say which path
request is out. Update checks, self-update and the new packages are
grouped as Installing and updating. Fixes to features new in this
release aren't listed: they never shipped broken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
A last job of the Release workflow, for a release (not a prerelease):

- Homebrew: the formula, written from the release's SHA256SUMS, is
  pushed to Formula/rettui.rb in the tap (zevaryx/homebrew-rettui, or
  the HOMEBREW_TAP variable's) with its deploy key
  (HOMEBREW_TAP_DEPLOY_KEY), GitHub's host key pinned as docs.github.com
  publishes it.
- AUR: rettui-bin's PKGBUILD, with AUR_USERNAME <AUR_EMAIL> as its
  maintainer and its commit's author, and .SRCINFO as makepkg writes it
  (in an Arch Linux container), are pushed with AUR_SSH_PRIVATE_KEY,
  once the AUR's host key matches the fingerprint aur.archlinux.org
  publishes. The first push makes the package.

Each half is skipped, with a notice, until its secrets are set. Pushing
is done by publish-homebrew.sh and publish-aur.sh, which push nothing
when the tap or the AUR has the release already, so the job can be
re-run. Checked against local repositories (an empty tap, one with a
README, a new AUR package; publishing, again, and the next release),
makepkg's .SRCINFO matches aur-pkgbuild.sh's, and actionlint and
shellcheck pass on the new job and scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Identity, in Status, has a Version line in both UIs: this version, what
kind of build it is (a release build and its target, or built from
source), and what the last update check found and when, or that one is
on its way, or why one asked for couldn't be made. With Check for
updates off and none asked for this run, it says the setting is off
rather than show an older answer. A newer release's Update row, with
Install, now sits under it, near the top.

`u` in the Status tab, or Check now in the web UI, checks at once,
whether or not daily checks are on, and says what it found: the newest
release, a newer one (shown and installable as one found daily), a
build newer than the latest release, or why it couldn't check. A daily
check already on its way is said the same way when it's in.

The web UI hears when a check or an install finishes (they finish on a
tick, which told browsers nothing), so Status shows it, and a check
asked for in the page toasts what it found.

The Status keys while first steps are shown had lost U (install update)
with the qol-2 changes; both lists now have u and U.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
Every announce heard goes to the announce viewer as a Heard event, which
the web UI counted as touching everything: browsers refetched all they
showed for each one, several times a second on a busy network. The
viewer asks for those after the last it has, and the Network list hears
the same announce as an Announce, so a Heard event touches nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tv5VJUy237EdS4U5P6JwjR
@zevaryx
zevaryx merged commit 857f29d into main Oct 8, 2026
11 checks passed

This branch was successfully deployed

1 active deployment
github-pages — 857f29d4 Deployed Oct 8, 2026 by zevaryx via Publish #10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants