Skip to content

Epic: every identity is a real Meshtastic node - #5

Merged
A13xB0 merged 79 commits into
mainfrom
epic/real-nodes
Sep 16, 2026
Merged

A13xB0 merged 79 commits into
mainfrom
epic/real-nodes

Conversation

@A13xB0

@A13xB0 A13xB0 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Epic: every RepeaterTastic identity becomes a real Meshtastic node, instead of RepeaterTastic re-implementing the node in Go.

Why

A Meshtastic maintainer's feedback on the raw modem PR (meshtastic/firmware#11863) was that meshtasticd is a full node, and a host should drive it through the client API the way Meshtasticator does rather than bypass it. Following that gives us every routing rule, module and future firmware change for free, and it needs no firmware change: SimRadio's SIMULATOR_APP envelope already carries frames both ways and honours RSSI/SNR.

The raw modem work (#4, the tcp:// KISS transport, meshtastic/firmware#11863, meshtastic/meshtastic#2702) is dormant and has been taken out of #3.

Shape

Every identity is a Node, and nodes join an Air. The GUI, chat, plugins and MQTT sit on top.

  • Node: a meshtasticd process we start with Lora: Module: sim, driven over the client API. The relay persona is a node too. Needs meshtasticd 2.8.0 or newer, installed or in Docker.
  • Air: Join, Leave and Relay(). One rule holds on every air: its nodes hear each other at hop 0, so nothing from the same mast is repeated.
  • LoRa air (now): our KISS modem or spi radio. Relay() is nil, so the host starts a hosted persona and joins it. Nodes' SIMULATOR_APP transmissions go out through the transmit queue (CSMA, duty cycle, site turns), and every heard frame is injected into every node with real RSSI/SNR.
  • Board air: a board on Meshtastic firmware (radio.driver: meshtastic). Relay() is the board, and joined nodes sit one hop behind it, carried over its MQTT client proxy.
GUI · chat · plugins · MQTT
        │
   nodes (meshtasticd × N, relay persona included) ──(client API)
        │
   Air ── LoRa air ── KISS modem / spi radio        (now: relay = hosted persona, nodes at 0 hops)
      └── board air ── Meshtastic board             (relay = the board, nodes 1 hop behind, via its MQTT proxy)

The relay's settings use Meshtastic's names: device roles (client, client_base, client_mute, router, router_late) plus monitor and off, and rebroadcast modes.

Child pull requests

Each merges into epic/real-nodes; this PR merges into main once they're all in (and after #3).

Removed

  • Identities running inside RepeaterTastic (no fallback when meshtasticd can't run: the top-bar chip goes red).
  • Experimental identities on several radios; identities can still move between radios.

Base

Branched from #3 (native-spi-radio), because hosted nodes use its spi driver. Until #3 merges, this PR's diff includes #3's commits.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc

random-thought and others added 23 commits September 15, 2026 21:35
radio.driver: spi drives an SX126x on a Linux SPI bus directly, with radio.device set to the
meshtasticd board file (config.d/lora-*.yaml). Pure Go: spidev and GPIO v2 chardev ioctls, no CGO.
49 of meshtasticd's 61 board files parse; LR1121, RF95 and CH341 USB adapters are rejected.

- internal/radio/sx126x: board parser (pins, gpiochip maps, Enable_Pins, SX126X_ANT_SW, TCXO,
  DIO2 RF switch, SX126X_MAX_POWER, TX_GAIN_LORA), Linux HAL, driver with datasheet errata,
  tests against a fake chip
- kisstool --board: info (SPI readback, device errors, noise floor), listen and send-text
- setup wizard keeps an spi radio's board file; editing a radio no longer forces kiss
- docs/spi-radio-testing.md: step-by-step guide for testers with hardware

Untested on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…, board names and auto

The native radio driver moves to internal/radio/spi and splits into a shared radio loop and chip
drivers, so it now covers every chip and bus meshtasticd's board files use:

- chips: SX1262/SX1268/LLCC68, SX1276/SX1278 (RF95), SX1280 (2.4 GHz) and LR1110/LR1120/LR1121,
  each from the datasheet and RadioLib's command sequences, with fake-chip tests for RX and TX
- CH341 USB-to-SPI sticks (MeshStick, Meshtoad, RAK19714, uMesh, PiNedio, PiggyStick) over usbfs,
  in pure Go
- boards: meshtasticd's 61 board files are built in; radio.device takes a file path, a board name,
  or auto (CH341 product string, Pi HAT+ EEPROM, RAK I2C EEPROM, as meshtasticd's autoconf)
- board file keys: per-chip power limits and defaults, LR11x0 rfswitch_table, optional BUSY,
  repeated YAML keys resolved like yaml-cpp (first wins)
- kisstool boards lists them; kisstool info prints each chip's diagnostics

LR2021 is left out: no meshtasticd board uses it. Untested on hardware.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A kiss radio's device can now be tcp://host:port. The driver dials it with a
connect timeout and TCP keepalives instead of opening a serial port, and
reconnects and re-applies the config exactly as for a serial modem. This is
how RepeaterTastic uses meshtasticd's raw modem mode, which serves a Linux LoRa
HAT or CH341 stick with the Mesh KISS v2 protocol (currently the raw-modem
branch of A13xB0/firmware).

- config: a malformed tcp:// device is rejected, and two radios can't share
  one (host names compared case-insensitively).
- kisstool: --dev tcp://host:port works.
- web: the setup "Test modem" probe accepts tcp:// devices; before a password
  is set it only dials loopback addresses.
- docs: meshtasticd-raw-modem.md, linked from the README, configuration and
  hardware pages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Edit radio and Add radio get a USB modem / meshtasticd switch: meshtasticd takes a host and port
and saves tcp://host:port, with a hint about RawModemPort. The setup wizard lists meshtasticd's raw
modem next to the serial ports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
FESC FESC TFEND decoded to FEND; the second FESC is an invalid escaped byte and
is dropped, so TFEND is then a literal 0xDC, matching the modem firmware.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
GET /boards (setup-level auth) lists what radio.device can be with driver
spi: auto, meshtasticd's board files in /etc/meshtasticd/config.d and
available.d, then the built-in copies, each with its module, bus and
whether this driver supports it.

POST /setup/probe takes driver: spi and opens the board, answering with
the chip's diagnostic lines in a new details field; a board a running
radio already drives is reported, not opened twice, and board files are
limited to /etc/meshtasticd since the probe runs before a password is set.

POST /setup takes driver (kiss or spi) and applies it with device; an
older wizard that sends only a serial port still can't overwrite an spi
board.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
ModemDeviceField switches between "USB modem" (the serial port as before)
and "Board (SPI or USB stick)": a select of the boards from GET /boards,
grouped by where they come from, with a board file path as the last
choice. It sets the radio's driver (kiss or spi) with the device, so Edit
radio and Add radio save both.

The setup wizard gets a "LoRa board on SPI or a USB stick" card under the
serial ports; Test modem probes the board and shows the chip's
diagnostics, and finishing sends driver with device.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
One lazy radio serves both drivers, so picking a Board in first-time setup
takes effect at once instead of showing the kiss driver until a restart.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
ModemDeviceField now offers all three ways to reach a radio: a USB KISS
modem on a serial port, meshtasticd's raw modem over TCP (from kiss-tcp),
and a LoRa board the spi driver runs itself. The setup wizard shows the
meshtasticd card and the board card under the serial ports. Picking
meshtasticd or a port sets driver kiss; picking a board sets spi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…uto finds it

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A reply left in the adapter by an interrupted session is longer than the next
short read expects, and usbfs answers EOVERFLOW ('value too large for defined
data type'). Read full 32-byte packets, and throw away leftovers when opening.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…ial port by name

experimental.meshtasticd_raw_modem gates radio.device tcp://…, the setup probe and
the meshtasticd choice in the modem pickers, until raw modem mode lands upstream
(meshtastic/firmware#11863). The setup wizard gets a card for typing a serial
port that isn't listed, and Windows COM ports pass the serial-path check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…ial port by name

experimental.meshtasticd_raw_modem gates radio.device tcp://…, the setup probe and
the meshtasticd choice in the modem pickers, until raw modem mode lands upstream
(meshtastic/firmware#11863). The setup wizard gets a card for typing a serial
port that isn't listed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
…tible

The same HAT has one meshtasticd file per host (Raspberry Pi, Luckfox Lyra
Zero W, OK3506…) and they all carry the same name, so the dropdown groups by
host and files found on this machine say where they came from.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
The raw modem mode isn't going upstream: meshtasticd stays a full node and
RepeaterTastic will drive meshtasticd instances over the client API
instead. Remove the tcp:// KISS transport, the experimental switch, the
meshtasticd choice in the modem pickers and setup wizard, and the guide.
The KISS decoder escape fix and its test stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Every identity becomes a real Meshtastic node: boards running Meshtastic
firmware attach over the client API, and the relay persona and identities
run as meshtasticd instances sharing our radio through an air bridge.
Child pull requests merge into this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
internal/mtclient speaks the client API to a board over serial or to
meshtasticd over TCP: the want_config handshake, a mirror of the node's
config, channels and node DB, admin requests that wait for the response or
the ack, and reconnecting after drops and reboots.

Live tests (RT_TEST_MESHTASTICD) bridge two sim-radio meshtasticd
instances the way the air bridge will. Measured on 2.7.26 and 2.8.0 and
written up in docs/meshtasticd-nodes.md: hosted nodes need 2.8, since 2.7
can't take a PKI DM back in; about 3 MB private memory per instance.

The interop harness gets UDP=0 to leave the sim radio as the only path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Real nodes 0+1: Meshtastic client API client and meshtasticd measurements
random-thought and others added 2 commits September 16, 2026 11:55
radio.driver meshtastic takes a node's serial port or a meshtasticd
address. internal/nodes.Attached is the radio and the one identity's
mesh.Remote: the node does its own routing, ACKs and encryption, and the
host keeps chat, the node DB, the packet log, traceroutes and app clients
from what the node delivers.

- mesh: remote identities (no private key). Sends go to the node,
  RemoteReceived takes its deliveries, periodic broadcasts and module
  replies are skipped, link frames are ignored, SwapRemote replaces a
  placeholder once the node answers.
- Settings both ways: UpdateConfig writes region, preset, slot, power,
  hop limit, primary channel and role to the node in one edit
  transaction; MirrorConfig and the config file follow the node.
- App ports: a node's identity can have one, and admin messages from the
  app are forwarded to the node.
- Web: setup and probe take the meshtastic driver (probe limited to this
  machine and the LAN before a password exists); names and channels are
  written to the node; extra identities, moves and key export are refused
  on a node radio; switching to or from a node needs a restart.
- GUI: radio switch USB modem | HAT or stick | Meshtastic node, a node
  card and probe in the setup wizard (which restarts by itself), node role
  wording, a Meshtastic node badge.
- mtclient/mtclienttest: a fake node shared by the tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Real nodes 2: a node on Meshtastic firmware as a radio
A13xB0 and others added 2 commits September 16, 2026 18:08
Hosted nodes get MQTT and UDP traffic
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A13xB0 and others added 2 commits September 16, 2026 18:09
Remove a scratch tool committed by mistake
Found testing a XIAO on Meshtastic 2.8.0 against the Pole.

- A board takes a PKI downlink only when it knows both nodes: the
  recipient is added as a contact (from the host's node DB) first.
  Identity -> Pole DMs now arrive, one hop out.
- Channels written to a board keep MQTT uplink and downlink on, so an
  edited channel still carries the identities.
- The notice and docs say that DMs to identities behind a board don't
  arrive: 2.8.0 publishes only what it can read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A13xB0 and others added 2 commits September 16, 2026 18:29
Board radios: direct messages out, channels keep the proxy
Found testing the CH341 bridge: the bridge rebooted and came back as a
new USB device, but the spi driver kept its old handle, reporting
"connected" while every transfer failed (1179 errors), and the lazy
radio never reopened a radio once it had opened.

- spi: a transfer failing because the adapter is gone (ENODEV,
  ESHUTDOWN, ENOENT, or EBUSY after a reset dropped our interface claim)
  closes the radio, and Stats reports it disconnected.
- lazy: when the radio closes (not on shutdown) it opens it again and
  applies the last settings.

Verified: after a USB reset of the bridge the radio reopened within a
second and received the Pole's next message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A13xB0 and others added 2 commits September 16, 2026 18:43
Radios reopen when their adapter goes away
The Go node stack is retired: the relay persona and every identity always
run as meshtasticd instances, with no fallback. The host keeps the air
bridge, transmit queue, packet log, node DB, chats, links, plugins and the
client API. Identities on several radios (federation) and the experimental
switches are removed; old config keys still load.

- GET /status has a nodes health object (ok, starting, warning, error),
  shown as a meshtasticd chip in the top bar that opens the new
  Configuration → meshtasticd tab
- the setup wizard's meshtasticd step is no longer optional
- the Docker image is now based on the meshtasticd image, and the Go
  build stage runs on the build platform
- deploy/install.sh sets up meshtasticd (--meshtasticd auto|apt|docker|skip)
- README quick start and docs updated

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A13xB0 and others added 4 commits September 16, 2026 21:03
Run every node on meshtasticd; status chip; packaging
deploy/install.sh now always installs meshtasticd 2.8+ (no options), and
downloads the latest release binaries for the machine when none are given,
checked against SHA256SUMS. The README quick start is two short paths, Pi or
Docker, then the browser; the device details moved to docs/hardware.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Shorter quick start: a Pi install or Docker
…able

Structure
- internal/web: handlers.go and extras.go split by area (setup, auth, status,
  identities, channels, messages, nodes, stats, config, backup, links);
  nodes.go (meshtasticd) renamed meshtasticd.go. Moves only.
- cmd/repeatertastic: radio startup moved to radio.go and split into
  openRadio, attachBoard and startLinks.
- internal/nodes: the settings a node is given moved to settings.go, and
  ApplyConfig split into small builders (cognitive complexity 78 -> ~15).

Fixes found by golangci-lint and SonarQube
- spi: NUL/0xFF padding was never cut from EEPROM and SX1280 version
  strings (strings.IndexAny/TrimRight work on runes, not bytes).
- errors wrapped with %w; unused parameters, fields and a deprecated
  MQTT field removed; deferred pin writes and cleanup made explicit.
- GUI: every button has an explicit type (form submits kept), unlabelled
  inputs and selects labelled, setup table has row headers, a mock bug.

Tooling
- .golangci.yml and `make lint` (0 issues), sonar-project.properties.
  Sonar: 0 bugs, 0 vulnerabilities, A ratings, duplication 0.1%.

GUI
- Configuration -> Position & hardware: drop a pin on a map (click or
  drag) to set the site position; map setup shared with the nodes map.
- Identities: one table of every radio's identities with a Radio column,
  and a "Port busy" status when an identity's app port can't be opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
A13xB0 and others added 10 commits September 16, 2026 21:42
Review: structure, lint and Sonar fixes; drop a pin; one identities table
The radio picker that reloaded the page is gone: every page shows the whole
site, with a radio filter where it helps.

API
- ?radio=all on /nodes (merged, with heard_by), /packets (radio_id),
  /links (radio_id, radio_name), /events (a status per radio) and the
  /stats endpoints. Single-radio /nodes has heard_by too.

GUI
- Top bar: one row per radio (connection, preset, airtime, its own relay
  mode) and the site's meshtasticd chip; unchanged for one radio.
- Dashboard, Statistics, Packets, Nodes, Links: radio filter and a radio
  column or chips; Dashboard has a per-radio overview.
- Identities, Channels, Chat: every radio's identities, labelled by radio.
- Configuration: Relay, Airtime, Position and MQTT have a "Settings for"
  radio selector; the site-wide tabs always show.
- Sidebar: each radio's relay persona.

meshtasticd
- Each instance keeps its last 1000 lines with timestamps, and its last 20
  stops with the reason. A stop right after RepeaterTastic committed
  settings is counted as a reboot (meshtasticd reboots to apply some), not
  as a failure, and doesn't warn in the status chip.
- GET /hosted/{name}/log; Configuration -> meshtasticd has a Log drawer per
  instance with its stops and output.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Replaces the full-width radio rows. The menu says what each mode does, and
the top bar sits above the page so menus aren't drawn under it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
- A radio with tx_power 0 (the region's limit) sent 0 while meshtasticd
  stores the limit it picked, so every reconnect pushed the LoRa config
  again and meshtasticd rebooted every 7 seconds. The resolved power is
  sent now, and a node that comes back without the same settings three
  times isn't pushed again: the log and the status chip say which
  settings it won't keep.
- Log lines carry radio and identity fields; meshtasticd nodes log with
  their identity. The Logs page labels each line and filters by radio or
  identity. Lines marked ERROR/WARN by the node code get that level.
- An identity whose app port can't open is logged once, not on every sync.
- The Logs page updates live again (the store replaced the array in place,
  which computed views don't notice).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Code smells (218 -> 0)
- Every function over cognitive complexity 15 split into named helpers
  (daemon startup, config validation, host receive/transmit, meshtasticd
  node settings, web handlers, plugin manager and API, CLI, mDNS, SPI chip
  drivers and board parser, KISS, MQTT, client API), tests included.
- Duplicated literals are constants, empty functions say why they're empty.
- GUI: nested ternaries and template literals unpicked, native elements
  instead of ARIA roles (fieldset, dialog, output, ul/li, hr), labels tied
  to their controls, modern string/number APIs. Copy falls back to a
  selected prompt where the Clipboard API isn't available (plain http).

Coverage (54% -> 95%): tests added for every package, with fakes for
modems, boards, meshtasticd, docker, MQTT brokers and plugin hosts. Test
seams only where a clock, a system path or os.Exit had to be swapped.

Bugs found and fixed on the way
- nodes: a board's delayed echo could send on the frame channel as it was
  closed (data race); sends and the close now share a lock.
- links/udp: when no multicast group could be joined, Run returned nil and
  left its output socket open.
- mesh: with no state dir, identities' positions were rewritten every
  250 ms after the first minute; the timer now runs once a minute.
- web: a generated identity key only avoided last-byte clashes on its own
  radio, so it could be impossible to move; every radio is checked now.
- spi: flaky and hardware-dependent board detection made testable.
- mqtt: the broker test expected in-process delivery; it now checks what
  hosted nodes get through the air bridge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
Site-wide GUI: every radio on every page; meshtasticd logs
…dentities; footer

- Identities have an "App can change node settings" switch (app_settings),
  off by default. Off, the app port refuses admin requests that change the
  node's radio, device, module or position settings, or reboot or reset
  it, with NOT_AUTHORIZED; reading, names, channels and the node list still
  go through.
- Removing a running radio lists its identities and moves them to another
  radio first (the default), or leaves them off air on disk.
- A footer on every page: Made in Scotland, linking ScotMesh, and the repo.
- Text about the old radio switcher updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SJZ75CWob1eMXeTa8Q2nLc
App settings switch, radio removal moves identities, footer
Docs: binaries on the PATH in the new image
@A13xB0
A13xB0 marked this pull request as ready for review September 16, 2026 22:50
@A13xB0
A13xB0 merged commit 1c3580e into main Sep 16, 2026
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