Skip to content

release: v0.3.0#52

Merged
mikevitelli merged 135 commits into
mainfrom
dev
May 26, 2026
Merged

release: v0.3.0#52
mikevitelli merged 135 commits into
mainfrom
dev

Conversation

@mikevitelli
Copy link
Copy Markdown
Owner

Automated release PR from /publish.

134 commits since v0.2.1: TUI framework rewrite, offline AI agent (uconsole-ai), Meshtastic mesh map, ADS-B feeder migration (dump1090 → readsb + viewadsb), AIO v2 rail dashboard, MimiClaw ESP32 chat portal, ESP32 Marauder hub + wardrive beta, antenna array braille ribbon monitor (MT7921 chains), hostname-aware Live Monitor header, configurable refresh rate, low-battery threshold retune for Samsung INR18650-35E cells, install ownership fix, postinst path cleanup, OSS-hygiene pass (operator-config snapshots stripped from source — .deb payload unchanged), CODE_OF_CONDUCT, refreshed README, v0.3.1+v0.4 roadmap, Next.js 16.1.6 → 16.2.6 (closes 19 advisories).

See CHANGELOG.md for the full breakdown.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

mikevitelli and others added 30 commits April 15, 2026 01:33
- Trim length (~485 → ~370 lines). Fold the "Features" bullet list
  into the three-tier intro; cut the exhaustive project-structure
  dump and the duplicated Makefile table.
- Bump the numbers: 9 TUI categories / 53 native tools / 997 device
  tests / 117 frontend tests. Refresh the TUI feature highlights
  (ADS-B global basemap, Telegram, Watch Dogs Go, ROM Launcher,
  shared launcher helper).
- Add a new "Polling and data flow" section with three Mermaid
  flowcharts: device→cloud telemetry timer (the only real polling
  loop), cloud dashboard reads (no polling — RSC on page load), and
  local webdash (on-demand + SSE only while Live Monitor is open).
- Call out the new CONFIG → Push Interval "off" option and document
  how to opt out of cloud telemetry entirely.
- Preserve all screenshot images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
git_sync committed without checking blob sizes. When a 164MB
.platformio tarball landed in ~ via backup.sh all, the nightly
push failed silently for 3 days while commits stacked locally on
the device.

Add git_sync_guard_blob_size: scans the staged index for any blob
over 95MB, names each offender by size and path, and aborts the
sync before commit. Offending files are unstaged so the working
tree stays clean for the next attempt.

.gitignore remains the single source of truth for "don't back
this up"; this guard only catches "can't push what we staged."

Tested: small file passes silently, 96MB file caught by name,
returns 1 with log_entry "ABORTED: oversize blob in index".
Package the 4 battery-safety services that were previously created
by hand pointing at ~/scripts/. After the pkg migration that path
no longer exists, so the units have been silently failing on every
boot — low-battery-shutdown retrying every 10s without ever
protecting the pack, pmu-voltage-min never setting VOFF to 2.9V.
This let the device brownout mid-sleep-wake when on battery.

Adds:
  packaging/systemd/pmu-voltage-min.service
  packaging/systemd/low-battery-shutdown.service
  packaging/systemd/cpu-freq-cap.service
  packaging/systemd/crash-log.service
  device/scripts/power/battery-safety.sh  (on|off|status helper)

All four ExecStart paths now point at /opt/uconsole/scripts/...
build-deb.sh picks them up automatically via the systemd/* glob.

UNSTABLE / opt-in semantics:
  - Fresh installs: units present but NOT enabled. User opts in with
    `battery-safety.sh on` or `systemctl enable --now`.
  - Upgrades: wants-target symlinks survive file replacement, so
    previously-enabled units stay enabled; just clears accumulated
    failed-state and restarts the daemon so the fixed ExecStart runs.
  - Cleans up orphaned ~/scripts/*.sh compat symlinks from the hand
    fix.

Untested in combination with the packaged layout — shipping to dev
for real-device verification before promoting to main. Do not cut
a release off this until we've confirmed the enable path works on
a pristine device.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a GPS-tagged war-driving feature spanning the TUI, webdash, and a
demo data generator. Landing on a WIP branch because the UX isn't
production-ready yet.

## What's in the bundle

### TUI (device/lib/tui/marauder.py)
- New "War Drive" tile in the Marauder menu (index 7)
- Continuous `scanap` with per-sighting CSV logging to
  `~/esp32/marauder-logs/wardrive-<ts>.csv` (WiGLE-ish schema)
- Persistent gpspipe poller thread maintains live TPV/SKY state
- Two views: braille-canvas map (APs + walked track + crosshair) and
  scrolling list; Tab/M/D to toggle
- Pulsing `● LOGGING` state banner, elapsed/rate/file-size header
- Exit confirmation modal with session summary + webdash URL
- Pause (X) / save-and-exit (B) with summary gate

### Webdash (device/webdash/app.py + templates/)
- GET /wardrive — MapLibre GL native-layer map with Carto Dark Matter
  basemap, heatmap + circle layers, click-to-popup, mobile-first DaisyUI
  (synthwave) navbar + FAB controls. No deck.gl, no Tailwind runtime.
- GET /wardrive?basic=1 — Leaflet fallback preserved at wardrive_basic.html
- GET /api/wardrive/sessions — list CSVs, newest first
- GET /api/wardrive/data/<name>?since=<idx> — delta-poll parsed rows
- Per-route CSP: adds basemaps.cartocdn.com, fonts.googleapis.com,
  jsdelivr.net, worker-src blob: for MapLibre
- Service Worker updated to never cache /wardrive and to invalidate
  when any template changes (not just app.py mtime)

### Demo generator (device/scripts/util/wardrive-demo.py)
- `static [min]` writes a complete CSV with a realistic walking path
  and scattered APs — use for UI testing indoors
- `live [min]` appends rows in real time (~1 Hz) to test the polling
  loop end-to-end
- `clean` removes DEMO-prefixed CSVs

## Feature flag (DISABLED BY DEFAULT)

All /wardrive and /api/wardrive/* routes return 404 unless one of:
  - file /etc/uconsole/wardrive-enabled exists
  - env UCONSOLE_WARDRIVE_ENABLED=1 in the webdash service

To enable on this device:
  sudo touch /etc/uconsole/wardrive-enabled
  sudo systemctl restart uconsole-webdash

## Why WIP
- Map UX is still being iterated on (pillars were bad, heatmap+dots
  feels better but isn't final)
- Performance on the CM4-class GPU needs more measurement
- TUI map view's Overpass street overlay is planned but not built
- Mobile responsive pass is functional but could use more polish
- No coverage analysis, KML export, or Wigle upload yet

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a real street background to the TUI War Drive map using OSM
highway geometry fetched from the Overpass API in the background.

**_OsmStreetFetcher** (new class, module-level)
- Persistent daemon thread watching live GPS fix
- Fetches highway ways in a 500 m radius around the current position
  with a 25 s Overpass timeout
- Caches responses to ~/esp32/marauder-logs/osm-streets-cache.json
  keyed by GPS rounded to 3 decimals (~110 m); bounded to 20 entries
- Re-fetches when position drifts >200 m from last query center, or
  when the cached entry is older than 1 hour
- Retries failed fetches after 60 s; otherwise idles 15 s
- Works fully offline once a neighborhood is cached

**_draw_wardrive_map** (updated)
- New `streets=` kwarg; when provided, draws polylines on a secondary
  braille canvas rendered underneath the APs/track/crosshair layer
- Per-cell merge — if the same cell contains both street bits and
  data bits, data color wins (bright green); else street bits render
  in C_DIM with A_DIM attribute for a muted gray backdrop
- Viewport auto-fit now includes first/last point of each street
  polyline, so the map is anchored to the neighborhood even with no
  AP sightings yet
- Scale caption shows street segment count

**_wardrive** main loop
- Instantiates _OsmStreetFetcher, starts on entry, stops in finally
- Pushes each GPS track sample to fetcher.update_position()
- New `S` key toggles streets on/off; footer reflects state
- Status bar shows "fetching…" on first enable and surfaces errors

**Smoke test (real network):**
- Around 40.7141,-73.9928 (Lower East Side) returned 1,391 polylines
  in under 5 s, cache file 175 KB on disk. Subsequent sessions in
  the same neighborhood hit the cache immediately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops Manhattan LES payload from 1,391 polylines to 249 (-82%) and
cache from 175 KB to 41 KB (-76%). Renders in two tiers so the grid
reads clearly in a dense city.

**Query-level filter** (Overpass):
- [highway~"^(motorway|trunk|primary|secondary|tertiary|unclassified|
  residential|living_street|*_link)$"] drops footways, sidewalks,
  cycleways, service roads, pedestrian zones. These are noise in a
  TUI overview.
- "out geom tags;" returns highway type alongside geometry.

**Tiered rendering**:
- MAJOR_TYPES (motorway/trunk/primary/secondary + links) → medium
  C_DIM so avenues and arterials stand out
- MINOR_TYPES (tertiary/unclassified/residential/living_street) →
  C_DIM | A_DIM so cross streets recede
- Data (APs/track/crosshair) on top at bright C_OK as before
- Per-cell priority: data > major > minor

**Viewport culling**:
- Quick lat/lon bbox test per polyline before projection
- Skips drawing for anything entirely off-canvas (plus 10% padding)
- Material win on dense neighborhoods where the 500m radius
  overshoots the visible view at high zoom

**Cache tuning for city use**:
- MAX_AGE_S: 1 h → 7 days (urban street grid rarely changes)
- MAX_CACHE_ENTRIES: 20 → 50 (city walks cover more unique bboxes)
- MOVE_THRESHOLD_M: 200 → 220 (~2-3 NYC blocks)
- Backward-compatible loader accepts legacy flat-polyline entries
  from v1 cache

**Scale caption** now shows "N_major / N_total major" count for
awareness.

Smoke test around 40.7141,-73.9928 (LES):
  1,391 → 249 polys; 175 → 41 KB cache.
  Types: {residential:104, secondary:70, primary:71, motorway:1}.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a HARDWARE → Meshtastic submenu that drives the locally-running
meshtasticd daemon over TCP 4403 via the official meshtastic Python CLI.
No bespoke chat protocol — entries map to status/nodes/listen/send/web/
logs and service start/stop/restart. Keeps the LoRa submenu intact for
point-to-point SX1262 use, with a note that meshtasticd must be stopped
first (both want exclusive SPI1).

New: scripts/radio/meshtastic.sh — thin bash wrapper around the CLI.
meshtastic --listen dumps raw protobuf + DEBUG log spam that's
unreadable in the TUI. Pipe through a small python filter that
prints one line per packet: [HH:MM:SS] PORTNUM from=!nodeid
plus MSG: <text> for TEXT_MESSAGE_APP. Raw view still available
via 'meshtastic --host localhost --listen' directly.
… entry

HARDWARE now has one "LoRa Mesh" submenu that drills into:
  • Chat (web UI), Send, Nodes, Listen, Status — the common actions
  • Config ▶ — privacy presets (stealth/public), MQTT on/off/toggle,
              position off/low/full/clear, rename, region, channel-name
  • Service ▶ — start/stop/restart/logs/web URL
  • Direct LoRa (P2P) ▶ — the old lora.sh point-to-point entries,
                          kept for advanced users who stop meshtasticd

Extends meshtastic.sh with a `config` subcommand covering the full
privacy + radio settings surface (mqtt, position, rename, region,
channel-name, and the privacy stealth/public macro presets).

Replaces the previous split where LoRa and Meshtastic each had their
own HARDWARE entry — with meshtasticd owning SPI1 in practice, the
split produced confusion and dead entries.
Reuses the ADS-B basemap renderer + projection to plot mesh nodes
from `meshtastic --host localhost --info` on the same curses
braille canvas. Home position is shared with ADS-B config.

Controls: +/- zoom, j/k cycle visible nodes, l toggle labels,
b toggle basemap overlay, h set home from GPS, r refresh, q quit.

Caches the nodes JSON for 30s; re-fetches in background.
Reviewed meshtastic.org/docs and reshaped the wrapper to mirror the
canonical CLI surface rather than my guessed shape:

* send — add send-dm (<!nodeid> <msg>), send-ack (--ack), send-ch
  (--ch-index N --sendtext). Rename "Send Message" → "Broadcast" +
  add "Direct Message" / "Broadcast + ACK" / "Send on Channel"
  TUI entries.
* reply — expose `meshtastic --reply` listen+echo mode.
* position low — use precision=13 (~2.9 km, the docs-canonical
  example) instead of an ad-hoc 10. Applies to privacy public preset.
* channel — full channel management: list, add (secondary), del
  (idx>0), psk (none|default|random|<hex>). New sub:lora_channels.
* power — reboot / shutdown / factory-reset (factory-reset requires
  typing RESET to confirm). New sub:lora_power.
* usage text annotates each block with the Meshtastic doc reference
  (canonical flags, channel-slot model, mqtt.*/position.*/lora.* keys).

The TUI sub:lora_mesh is now the single entry point covering every
canonical operation docs describe — message send variants, channels,
config, service, power, and direct LoRa P2P as the escape hatch.
Design doc for replacing the hardcoded MIMI_IP with serial-based IP
discovery, adding a WiFi config panel (scan / copy-from-uConsole /
manual), and distilling the ESP32 submenus (−4 top-level items per mode,
Reflash/War Drive/Settings subfolds for destructive or growable ops).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles current working-tree changes onto wip/wardrive-map so that
filter-repo can run against a clean tree. Includes wardrive map,
meshtastic map updates, adsb basemap work, webdash wardrive backend,
MimiClaw TUI module (untracked), and battery-safety edits.

Unpackable via 'git reset --soft HEAD~1' after the purge completes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
device/scripts/system/backup.sh wrote WiFi PSKs, SSH keys, and sudoers
rules into whatever repo it was invoked from. When an automated run
picked it up from this public tree on 2026-04-17 (commit 0881682), it
committed five .nmconnection files with plaintext PSKs to origin/dev.

Root cause: the script existed in two places — this public repo and
the private ~/pkg repo. It belongs only in ~/pkg.

Fix (defense-in-depth):
  - Delete backup.sh from this repo entirely. One source of truth.
  - Gitignore device/scripts/system/wifi/ and other credential-bearing
    paths so future accidents can't recommit them.
  - History has been rewritten via git-filter-repo to purge the WiFi
    .nmconnection files from every commit on every ref.

PSKs should be considered compromised (public for ~5 days on GitHub).
Rotation is a separate concern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous launcher searched two trees (/opt/uconsole/lib AND
~/uconsole-cloud/device/lib), preferring the dev tree. That made
"where is this TUI actually running from?" ambiguous and coupled the
launcher to a specific dev-tree path that only exists on the author's
machine.

New behaviour: read from ~/pkg/lib if it exists, else fall back to
/opt/uconsole/lib. Switching between stable and in-flight TUI code is
now a git branch switch in ~/pkg/ — no env vars, no precedence rules,
no second search path.

On boxes without ~/pkg/ (ordinary APT-only installs) nothing changes:
/opt/uconsole/lib is still the active tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Meshtastic integration for v0.2.2: TUI submenu, mesh map, LoRa/Meshtastic
consolidation under a single HARDWARE entry, one-line packet-summary
filter for the Listen view, and a meshtasticd wrapper aligned with the
canonical Meshtastic docs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous logic ran 'git describe --tags --always' against _PKG_ROOT
and parsed the result. When _PKG_ROOT is ~/pkg (the new default after
the console launcher refactor), that reads ~/pkg's tags — which do not
track uconsole-cloud releases. Result: TUI showed '0.1.6-dev' while
VERSION said 0.2.1.

Fix: read VERSION unconditionally. Append '-dev' when running from any
non-installed tree. Simpler, correct, and survives future tag drift in
whichever repo hosts the TUI source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earlier commit (0d36d98) moved the launcher to prefer ~/pkg/lib over
/opt/uconsole/lib. That gave ~/pkg a runtime role it was never meant
to have, and created the version-string bug fixed in 2d81b91: pkg
has its own git tags that don't track releases, and anything reading
metadata from pkg got the wrong answer.

Back to one runtime source: /opt/uconsole/lib. Pkg is private backup
only — it holds WiFi/SSH creds and package manifests, never runtime
code consumers should import from.

For ad-hoc testing of an alternate tree, set UCONSOLE_DEV_LIB=/path.
No implicit dev override — what runs is always what `make install`
last deployed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sibling to console-pkg. Sets UCONSOLE_DEV_LIB to ~/uconsole-cloud/device/lib
so the TUI loads source code from the active dev checkout instead of the
installed /opt/uconsole tree.

Use case: preview uncommitted or pre-install edits without running
make install. Intended to be bound to Ctrl+\` at the compositor layer
while Ctrl+Shift+P stays on console-pkg (installed/published TUI).

Falls back to installed if the dev tree is missing (e.g. on end-user
devices without the source checkout).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VERSION tracks the last released version. Dev work is always building
toward the next one, so the running TUI should report where the work is
headed — not where the last release sat. 0.2.1 → 0.2.2-dev.

Falls back to plain '-dev' suffix if VERSION has a non-numeric tail
(pre-release tags like 0.2.1-beta etc.) so we never crash on version
parsing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three high-impact fixes aligning CONTRIBUTING.md with the current
architecture:

1. Versioning section was describing a git-describe-based scheme that
   was replaced today (commit ab2a3f6). Now documents VERSION-file +
   patch-bump semantics so contributors understand why console-dev
   shows 0.2.2-dev while the last release was 0.2.1.

2. Device development section now names the three launchers
   (console-dev / console-pkg / console) and their keybinds, so
   contributors know which one to use for the iteration loop without
   reading source.

3. TUI module list grew from 11 entries to 22 — the old list missed
   mimiclaw, telegram, watchdogs, meshtastic_map, launcher, and five
   adsb files. Missing modules made the "what does this file do?"
   question unanswerable from the docs alone.

Does not touch script count, test count, or tool count — those need a
separate recount pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three changes to reduce root-level sprawl and document the full
release flow:

1. New docs/PIPELINE.md — maintainer reference for the edit → preview →
   commit → CI → publish → user chain. Not contributor-facing (that's
   CONTRIBUTING.md's job). Covers the three speeds (flicker/commit/
   release), automation triggers, manual decision points, and failure
   modes.

2. Move FEATURES.md → docs/FEATURES.md. Updated the 5 references in
   CONTRIBUTING.md, SECURITY.md, and three Claude slash-commands.

3. Move ADSB_BASEMAP_PLAN.md → docs/plans/ADSB_BASEMAP_PLAN.md. No
   references existed; this is a historical feature plan that now lives
   alongside any future plans in a single directory.

Root now holds only the docs GitHub conventions expect there:
README.md, CONTRIBUTING.md, LICENSE, SECURITY.md, CHANGELOG.md, VERSION.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause of "ESP32 hub shows Unknown with MimiClaw flashed":

1. MimiClaw detection branch + TUI module lived only on wip/wardrive-map
   (commit d85afc6), never merged to dev. Installed /opt/uconsole/lib
   had no MIMICLAW enum value, no mimiclaw.py, and no menu wiring.
2. Even the wip branch's probe strings were wrong — checked for
   "mimi:" and "Type 'help'" but the real firmware emits "mimi>" as
   its prompt after any newline.

Evidence (device at /dev/ttyACM0 running MimiClaw v8390390 today):
  Ctrl-C×2 + \r\n  →  '\r\nmimi>  \r\nmimi>  '   ← marker present
                                                   in the same response
                                                   Phase 1 already reads

Fix is surgical — cherry-picked from wip without pulling in unrelated
WIP work (wardrive menu additions, GUI launcher helpers, version
regression):

- device/lib/tui/mimiclaw.py (369 lines, new)
    Chat / Serial / Status / Flash handlers.
- device/lib/tui/esp32_detect.py (+9 lines)
    MIMICLAW enum value + Phase 2.5 probe that checks for "mimi>" in
    the existing Phase 1 response. No extra serial round-trip.
- device/lib/tui/framework.py (+~30 lines)
    _ESP32_MIMICLAW_ITEMS submenu list + dispatcher entries
    (_mimiclaw_chat, _mimiclaw_serial, _mimiclaw_status,
    _esp32_force_mc) + badge + flash picker entry.

Verified end-to-end:
  detect() → Firmware.MIMICLAW
  tui.mimiclaw.run_mimiclaw_{chat,serial,status,flash} all importable

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MIMI_IP was hardcoded to 192.168.1.23. Breaks every time DHCP reassigns,
and broke today when the device roamed to a different AP and picked up a
different address. The spec at
docs/specs/2026-04-22-mimiclaw-wifi-and-esp32-tui-refactor.md Part 1
called for cache + serial-probe fallback. This implements exactly that
scope, no more.

What changed in tui/mimiclaw.py:
  - Delete MIMI_IP constant.
  - Add _load_cached_ip / _save_ip — JSON at
    ~/.config/uconsole/mimiclaw.json, chmod 600, atomic tmp+rename.
  - Add _probe_ip_via_serial — sends 'wifi_status', parses 'IP: x.y.z.w',
    returns None on 0.0.0.0 or any parse failure. Short-lived serial
    matching the existing _query_mimiclaw_status pattern.
  - Add _resolve_ip(prefer_fresh=False) — cache → probe → None.
  - Rewrite connect_ws in run_mimiclaw_chat:
      - First attempt uses cache (fast path)
      - On connect failure, re-probe serial once and retry
      - On second failure: surface the real error ("device offline" or
        "connection failed: <reason>") instead of a generic timeout

Verified end-to-end against live MimiClaw at 192.168.1.23 on
Big Parma - 2.4GHz: unit tests pass (5/5 including 600-perm check,
junk rejection, live probe, cache population) and resolved IP is
correct.

Does NOT implement Part 2 (WiFi config panel / Settings subfold).
That's a separate commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a WiFi config screen reachable from MimiClaw ▸ Settings ▸ WiFi,
completing Part 2 of docs/specs/2026-04-22-mimiclaw-wifi-and-esp32-
tui-refactor.md.

Method picker with four flows:
  - Scan: send wifi_scan, parse '[N] SSID=X RSSI=Y CH=Z Auth=W' lines,
    sort by RSSI desc, de-dup empties, render list with signal bars
    and lock glyph for secured networks.
  - Copy from uConsole: sudo nmcli to read the device's currently
    active WiFi SSID + PSK, confirm, apply.
  - Manual: two text fields (SSID allows spaces, password masked with
    Ctrl-R reveal).
  - Disconnect: sends set_wifi "" "" with confirm. Behaviour on real
    firmware is empirically untested; surfaces any error plainly.

Apply pipeline (shared across all four flows):
  1. set_wifi + wait ≤3s for "WiFi credentials saved for SSID:"
  2. restart + 10s progress screen
  3. poll wifi_status every 2s for up to 15s, looking for real IP
  4. on success: update ~/.config/uconsole/mimiclaw.json cache
  5. on failure: surface specific error (port busy / timeout /
     SSID out-of-range etc) — no generic "connection failed"

New pure/testable helpers:
  - _wifi_scan_parse(raw) — regex + sort + de-dup, returns list of dicts
  - _format_apply_payload(ssid, password) — quoted serial payload,
    rejects \r/\n, escapes embedded double-quotes
  - _signal_bars(rssi) — block glyph for signal strength

Wiring:
  - _ESP32_MIMICLAW_ITEMS gains a Settings submenu entry
  - SUBMENUS["sub:mimiclaw:settings"] registered with WiFi item
  - _mimiclaw_wifi dispatch token added

Verified:
  - Pure-function unit tests pass (scan parse, payload formatting,
    disconnect form, quote escaping, newline rejection)
  - syntax clean on both files

Distillation across MicroPython/Marauder/Common footer is a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Spec-mandated common footer distillation (docs/specs/2026-04-22).

Before (6 items, duplicated):
  Install Bruce           ← redundant: Reflash picker already has Bruce
  USB Reset
  Switch Firmware         ← renamed below
  Backup FW
  Clear FW Cache          ← rarely used, kept as dispatch token only
  Re-detect

After (4 items, recovery first):
  USB Reset               ← most common fix when something is broken
  Re-detect               ← recovery — when the hub shows wrong firmware
  Backup FW               ← safety snapshot before reflash
  Reflash                 ← renamed from Switch Firmware for clarity;
                            unchanged picker UI with all four firmwares

Dropped from the menu but handlers still exist (one-line re-add if
needed): "_esp32_install_watchdogs" and "_esp32_fw_cache_clear".

The full Reflash ▸ subfold design from the spec (with one-tap per
firmware + Clear Cache inside) requires refactoring the picker to
accept a preselect arg — deferred to a follow-up commit. This is the
minimal distillation that matches what the spec promised at the top
level: "4 top-level items, recovery first, destructive behind ⇄".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-firmware distillation from docs/specs/2026-04-22, applying the
cross-cutting patterns:
  - Position 1 = primary action (what you open the submenu for)
  - Position 2 = Serial Monitor (muscle memory across firmwares)
  - Position 3 = Status (always "firmware info + chip rollup")
  - Drop every duplicate; the common footer has the canonical verbs

MicroPython (8 → 6):
  before: Status | Live Monitor | Serial Monitor | REPL | Flash Scripts
          Reset | Log Entry | Chip Info
  after:  Live Monitor | Serial Monitor | Status | REPL
          Flash Scripts | Log Entry

  Live Monitor promoted to position 1 (the daily use case).
  Status description widened to cover "chip info" so the dropped
  Chip Info entry isn't missed.
  Reset dropped — duplicates the common USB Reset.

Marauder (6 → 4):
  before: Marauder | Serial Monitor | Scan APs | Device Info |
          Settings | Reboot
  after:  Marauder | Serial Monitor | Status | Settings

  "Device Info" renamed to Status for cross-firmware naming consistency.
  Scan APs dropped — Marauder's own in-app menu already has it, so the
  top-level shortcut was one extra way to do the same thing.
  Reboot dropped — duplicates the common USB Reset.

MimiClaw (already distilled in 5345632: Chat | Serial | Status | Settings).

Total: 14 items removed across the three modes. Each now has 4–6
focused entries plus the shared 4-item footer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mikevitelli and others added 23 commits May 9, 2026 18:25
framework.py treats VERSION as the last *shipped* version and a dev
checkout displays it as `(VERSION + 1)-dev`. Commit 9583f11
("release: prep v0.2.2") bumped VERSION ahead of cutting v0.2.2 — but
v0.2.2 was never tagged, so VERSION drifted to 0.2.2 while the latest
tag stayed at v0.2.1. Result: the dev TUI showed 0.2.3-dev instead of
the expected 0.2.2-dev.

Reverted VERSION and device/VERSION to 0.2.1 to restore the invariant.
The v0.2.2 prep work in CHANGELOG.md stays — it documents what's
queued for the next release. When `make release` cuts v0.2.2, the
bump-patch step will advance VERSION to 0.2.2 atomically with the
tag, exactly as the release flow expects.

Verified: dev tree resolves to PKG_VERSION = '0.2.2-dev'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adapts the gamepad reader and trackball-scroll daemon to the new bb9900
ZMK keyboard. ZMK firmware emits ABXY as F21–F24 over the keyboard HID
interface and exposes no joystick node, so we side-channel-read the
keyboard's evdev node without grabbing it. trackball-scroll switches
from KEY_SELECT to KEY_FRONT (Fn-alone) on the new device names.

wait_for_input migrated to read EVDEV_SIZE chunks and unpack with
EVDEV_FMT, returning only on key-down for codes in _KEY_TO_GP — fixes
a runtime NameError on the old JS_SIZE/JS_FMT constants that the
initial port missed.

Verified: pyflakes clean, 1142 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stock uConsoles use the built-in brcmfmac (wlan0) as the only radio,
but it's possible to route antennas to a USB adapter (e.g. AC12000 →
wlan1). Replace the wlan0 hardcode with a sourced config-file override:
default stays wlan0, devices that need a different radio drop a single
WIFI_IFACE= line into /etc/uconsole/wifi.conf.

Also: harden wifi-fallback.sh against being run as an NM dispatcher,
where HOME is unset and `set -u` made the $HOME/.config STATE_DIR
lookup crash. Falls back to UID 1000's home.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Working-tree scrub of personal data that had accumulated across code,
tests, fixtures, docs, and the .deb staging dir:

- wifi.sh / wifi-fallback.sh: hardcoded NM connection names
  (HOME_CON / OFFICE_CON / IPHONE_CON) now sourced from
  /etc/uconsole/wifi.conf via WIFI_HOME_CON / WIFI_OFFICE_CON /
  WIFI_IPHONE_CON. Empty defaults; the home/office/iphone shortcuts
  print a "not configured" error if the corresponding var is unset.
  fallback dispatcher's iPhone path is a no-op when WIFI_IPHONE_CON
  is empty.
- monitor.py: hardcoded "WiFi: Big Parma - 2.4GHz" sample replaced
  with "WiFi: -" placeholder (the line was never wired to live data).
- wifi_radio.py / iw_dev.txt fixture / parser test: anonymized SSIDs
  and MACs.
- wardrive demo + preview ESSID pools: replaced a real phone-hotspot
  SSID with a generic placeholder.
- docs (bookworm-migration, two AIO-v2 specs/plans, mimiclaw spec,
  TUI audit): replaced real SSIDs / LAN IPs / a literal WPA password
  with <placeholder> tokens.
- device/scripts/ssh/id_ed25519.pub: removed. Was already excluded
  from the .deb by packaging/build-deb.sh, but had no business sitting
  in the canonical tree.

History rewrite is a separate decision — this commit only stops new
clones from picking up the data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local CLI agent that talks to the on-device ollama daemon (default
qwen2.5:7b). Stdlib-only Python — no extra deps. Tools: run_bash
(confirms on mutating commands like sudo/rm/mv/dd/chmod/systemctl/
apt/git-push/redirects), read_file, write_file, list_dir.

Modes:
- one-shot: uconsole-ai "query"
- REPL: uconsole-ai (with /reset, /model X, /exit)

Flags: -y auto-confirm, -q hide tool traces, --full-context inline
CLAUDE.md, --list-models. The shell shim auto-detects whether to
run from the dev tree or /opt/uconsole/lib/.

Tested on CM5 Lite 16GB (no GPU) — qwen2.5:7b is the working default;
~2 tok/s gen, 60-195s per agent turn. Use case: "send query, walk
away," not interactive chat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-event motion-passthrough/wheel-emit/grab-OK lines were flooding
journalctl during normal operation, drowning out everything else
when reviewing system logs. Gate them behind a TRACKBALL_DEBUG env
var so the daemon stays quiet by default; failures (grab=FAIL,
ungrab=FAIL) remain unconditional since those still want surfacing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI install-test "no personal data in package" check (Dockerfile.test:114)
caught two leaks in the initial uconsole-ai commit:
- a hardcoded `/home/mikevitelli/CLAUDE.md` path in the resolver list
  (Path.home() / "CLAUDE.md" already covers the right case)
- "User: mikevitelli." in the fallback system-prompt string

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pick the lowest-metric default route to label the NET panel — shows
"Wired <ip>" when ethernet is plugged in instead of always falling
back to the wifi SSID. Also reads signal from the active wifi
interface (was hardcoded to wlan0, which is unmanaged on AC12000).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds CONFIG → Monitor Refresh picker (250ms / 500ms / 1s / 2s / 3s / 5s),
persisted as monitor_refresh_ms in console.json. run_live_monitor reads
the saved value (default 1000) and the footer label reflects it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds --chown=root:root to the rsync calls in `make install` so files
under /opt/uconsole/ are owned by root regardless of who ran the
command. Previously they inherited the invoking user's ownership,
which is wrong for a system-installed package.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…isable

State directory was under $XDG_CONFIG_HOME/wifi-fallback (per-user
$HOME), but the script runs as a NetworkManager dispatcher under root
with no useful HOME. Move state to /var/lib/wifi-fallback where it
belongs for a system daemon and drop the HOME-fallback gymnastics.

Also gate enable/disable subcommands behind a sudo re-exec so users
running them interactively don't silently fail to create /var/lib
entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native TUI screen for the MT7921 (AC1200) 2x2 array. Chain A is the top
trace, B the bottom, and the filled braille band between them is colored
by the |A-B| delta — a marginal connector shows as the band fattening and
going red. Reuses tui_lib.BrailleCanvas + framework input/colors, same
class as GPS Globe / FM Radio.

- EMA-smooths each chain before drawing (raw per-poll RSSI speckles)
- auto-ranged Y (min_span 10, pad 0.15) so wiggles show even at -32 dBm
- widened balance thresholds: <=6 balanced / <=12 drifting / >12 check
- fixed fast 250ms refresh, one BLE spinner, no flashing/ambient
- self-contained module; framework import-isolation keeps a fault from
  affecting the rest of the menu

Registered tui.antenna in FEATURE_MODULES + sub:wifi menu entry.
Replace hardcoded "UCONSOLE" in the scanline header with
socket.gethostname().upper(), cached once before the render loop.

The TUI is the same package installed on all uConsole-class
devices via apt.uconsole.cloud — the header now reads
"◈ UCONSOLE ──", "◈ SERVER ──", "◈ PI-TOP ──" etc. per
host instead of a misleading constant. Falls back to UCONSOLE
if gethostname() returns empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .claude/commands/ slash-command prompts and .vscode/settings.json
(which only contained `claudeCodeChat.permissions.yoloMode: true`)
are local developer ergonomics, not part of the public package. Strip
them from the source tree.

Harden .gitignore so future AI-tool scratch dirs (.cursor, .copilot,
.continue, .aider*, AGENTS.md, GEMINI.md) and credential-flavored
filenames (*.key, id_*, secrets/, .config/op/) can't sneak in via
`git add .`. No tracked files match the new patterns today — these
are defensive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove 33 files that were operator's personal /etc and user-session
state, not part of the package payload:

- device/scripts/config/gh/{repos.txt,config.yml} — personal GitHub
  repo list (incl. private repos) and gh CLI prefs
- device/scripts/config/pulse/cookie — PulseAudio auth cookie
- device/scripts/config/systemd-user/{claude-relay-agent,evolution-*,
  goa-daemon,gvfs-*,webdash}.service — operator's GNOME/Evolution
  user-session services and a webdash.service with hardcoded
  /home/mikevitelli/scripts/webdash.py and a default WEBDASH_PASS.
  The package installs packaging/systemd/uconsole-webdash.service,
  which is the canonical version. trackball-scroll.service stays.
- device/scripts/config/{chromium,dconf,gtk-3.0,gtk-4.0}/ — desktop
  state dumps (themes, prefs, dconf-dump.ini with NM eap UUIDs)
- device/scripts/config/{mimeapps.list,themes.txt}
- device/scripts/system/etc/{fstab,hostname,hosts,crontab.user,
  sshd_config,keyboard,locale,timezone} — fstab pinned a specific
  PARTUUID, crontab.user had hardcoded /home/mikevitelli/ paths
- device/scripts/system/etc/sudoers.d/* — operator's env-keep tweaks
- device/scripts/system/alsa/asound.state — operator's audio mixer
  state; restore.sh reads from the private backup repo ($REPO_DIR),
  not from this tree, so removing here is safe

build-deb.sh already scrubs scripts/system/etc and scripts/config
from the .deb (lines 63, 65), so this commit changes nothing about
what end users install — it only removes the source-tree exposure
on GitHub.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ifacts

- Consolidate three docs trees into two:
  - device/docs/troubleshooting.md → docs/TROUBLESHOOTING.md
  - device/docs/README.md (was just a cloud-docs redirect) deleted
  - docs/{audits,plans,specs} → docs/internal/{audits,plans,specs}
    separates public-facing docs (ARCHITECTURE, API, FEATURES, etc.)
    from internal planning trail
  - device/webdash/docs/ kept as-is (served by the webdash wiki panel)

- Fix device/scripts/util/lib.sh broken symlink: was pointing at
  /opt/uconsole/lib/lib.sh (broken in any fresh clone before
  `make install`). Now a relative symlink to ../../lib/lib.sh
  so the dev tree works straight from git.

- Remove device/lib/tui/adsb_basemap_global.json.bak (482KB dead
  backup file, unreferenced; only adsb_basemap_global.json is
  read by adsb.py:128).

- packaging/copyright Upstream-Contact: mike@chopcheese.nyc →
  mike@uconsole.cloud to match packaging/control's Maintainer
  field and avoid surfacing the personal domain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
packaging/copyright had mike@chopcheese.nyc (personal/business
domain) while packaging/control's Maintainer field already used
mike@uconsole.cloud. Match them so apt-cache pages and
dpkg-deb -I output show consistent contact info.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
README rewrite (211 → ~290 lines):

- Tagline reframed from "remote monitoring" to "complete software
  stack — TUI, web dashboard, and optional cloud telemetry" to match
  what's actually shipping. The device side has outgrown the cloud
  half by a wide margin.
- TUI section corrected: 23 feature modules across 9 categories
  (was "9 categories, 64 native handlers"). Highlights per category.
- New "Subsystem highlights" section walks the marquee shipping
  pieces one paragraph each: offline AI agent, MimiClaw, Marauder +
  wardrive, Meshtastic (with the SX1262 init fixes that AIO v1 boards
  needed), ADS-B (readsb migration), antenna array monitor,
  battery/power hygiene, WiFi suite.
- "Hardware target" section explicitly names CM4 and CM5, AIO v1/v2,
  MT7921 AC1200, SX1262 LoRa, and documents the PCIe Gen 2 derate.
- Security table picks up 4 new rows: push rate-limit/shape-check,
  device-pushed wifi.ip validation, shell hardening (eval/source
  replacement), CI SHA pinning.
- "Related repos" section calls out the mikevitelli/uconsole private
  backup repo so newcomers don't confuse the two.
- License section added.

CHANGELOG.md: rename "v0.2.2 (unreleased)" → "v0.3.0 (2026-05-26)",
add a sentence covering the items that landed since the heading was
first drafted (uconsole-ai, antenna monitor, hostname-aware live
monitor, OSS hygiene pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README + CONTRIBUTING.md: "100+" / "47" management scripts → "50+"
  (actual count under device/scripts/ is 51 — pick a number that holds
  in both docs)
- CHANGELOG.md: add "## (unreleased)" section above v0.3.0 so v0.4
  work has a clear landing spot. Drop the stale "What's next (v0.1.8+)"
  stub at line 233 — all four items shipped or no longer relevant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reference-style file (links to contributor-covenant.org rather than
inlining the full 2.1 text) — standard practice, recognized by
GitHub's community-profile detector. Brings community profile from
85% to 100% and removes the "Add CODE_OF_CONDUCT.md" banner GitHub
shows on the Insights tab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After the v0.3.0 prep, a 6-track audit (OSS front door, docs
completeness, code quality, security, roadmap coherence, ops &
observability) surfaced ~100 findings. The pre-publish quick fixes
landed in v0.3.0; the rest is captured here so it doesn't drift.

Structured into:
- v0.3.1: 11 close-the-gap items (workspace-monitor decision,
  default webdash password, session invalidation, observability
  quick wins, OSS-front-door polish)
- v0.4: 17 bigger lifts (marauder.py split, first-5-min guide,
  HW compatibility matrix, runbook, dependabot, etc.)
- Conditional: multi-device backbone, suspend-to-RAM, workspace
  gamepad isolation enhancement

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Non-major bump that closes ~19 stacked advisories on the prior
version:

- CVSS 8.6 SSRF (GHSA-c4j6-fc7j-m34r)
- CVSS 8.1 Middleware bypass via dynamic route params (GHSA-492v-c6pp-mqqv)
- CVSS 7.5 DoS × 3 (GHSA-q4gf-8mx6-v5v3, GHSA-8h8q-6873-q5fj, GHSA-26hh-7cqf-hhc6)
- Plus XSS (GHSA-gx5p-jg67-6x7h) and other moderate findings

Verified: `npm run lint` clean, all 212 vitest tests pass.

Remaining npm-audit findings (6 moderate, 4 high) are transitive
dev-time deps (vite, picomatch, flatted) — not in production
bundles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mikevitelli mikevitelli enabled auto-merge (rebase) May 26, 2026 06:59
@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
uconsole-cloud Ready Ready Preview, Comment May 26, 2026 7:11am

Request Review

auto-merge was automatically disabled May 26, 2026 07:12

Rebase failed

@mikevitelli mikevitelli merged commit 4137e0a into main May 26, 2026
7 checks passed
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.

1 participant