Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ These are not blocked, just reviewed more carefully.

## Workflow

1. Fork the repo (or, if you're a collaborator, push a feature branch directly to `KMX415/meshpoint`)
2. Create a branch from `main`
1. Fork the repo
2. Create a branch from main
3. Make your change
4. Test it
5. Open a pull request **targeting `KMX415/meshpoint:main`**
5. Open a pull request

Example branch names:

Expand All @@ -65,12 +65,6 @@ docs/setup
refactor/config-loader
```

When you open the PR from a fork, **leave "Allow edits from maintainers" checked** (GitHub's default). It lets the maintainer push small fixes (lint, rebases on top of newly-landed work) directly onto your PR branch without a back-and-forth review cycle.

PRs land on `main` via **Squash and merge** by default, so each merged PR becomes one commit. Keep your PR commit message in the PR description (the merge UI uses it).

If `main` moves while your PR is open and conflicts appear, rebase your branch on the new `main` and force-push to your fork. Don't merge `main` into your PR branch -- it produces a noisy history that breaks the squash-merge convention.

---

## Pull request expectations
Expand Down Expand Up @@ -134,15 +128,12 @@ Rules:
## Before opening a PR

- Code builds
- Tests pass: `python -m pytest tests/ -q`
- Lint clean: `python -m ruff check src/ tests/`
- Tests pass (if present)
- Docs updated if needed
- Config changes documented
- Hardware/region impact noted
- PR description is clear

CI runs the same `ruff` + `pytest` jobs on every PR (see `.github/workflows/ci.yml`). PRs cannot merge until that check passes.

---

Meshpoint is evolving quickly. Process will stay simple for now.
31 changes: 11 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
[![GitHub stars](https://img.shields.io/github/stars/KMX415/meshpoint?style=flat&color=yellow)](https://github.com/KMX415/meshpoint/stargazers)
[![GitHub issues](https://img.shields.io/github/issues/KMX415/meshpoint)](https://github.com/KMX415/meshpoint/issues)
[![Last commit](https://img.shields.io/github/last-commit/KMX415/meshpoint)](https://github.com/KMX415/meshpoint/commits/main)
[![Version](https://img.shields.io/badge/version-0.7.3.1-orange.svg)](docs/CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-0.7.2-orange.svg)](docs/CHANGELOG.md)

### Meshradar Cloud Dashboard
![Meshradar Cloud Dashboard](Meshradar414.png)
Expand Down Expand Up @@ -137,7 +137,7 @@ sudo meshpoint setup # interactive config wizard
meshpoint status # verify everything is running
```

Open `http://<pi-ip>:8080` for the local dashboard. On first visit (and after upgrading from v0.7.2 or earlier) you'll be prompted to set an admin password at `/setup` (8-character minimum). After that, all dashboard access requires sign-in. If you forget the password, recover via SSH with `sudo meshpoint reset-password` -- the command prompts interactively, rotates the JWT secret, and invalidates any open browser sessions.
Open `http://<pi-ip>:8080` for the local dashboard.

> **First time?** The [Onboarding Guide](docs/ONBOARDING.md) walks through everything from flashing the SD card to verifying your first captured packets.

Expand Down Expand Up @@ -210,33 +210,24 @@ sudo git pull origin main
sudo systemctl restart meshpoint
```

The local dashboard shows an orange update indicator when a new version is available. After every update, hard-refresh the dashboard tab (Ctrl+Shift+R / Cmd+Shift+R) so the browser pulls the new frontend JS instead of a cached copy.
The local dashboard shows an orange update indicator when a new version is available.

### Upgrading from v0.7.2 or earlier to v0.7.3 (one-time)
### Updating to v0.6.0 (one-time steps)

v0.7.3 adds local dashboard authentication and pulls in two new Python dependencies (`bcrypt`, `PyJWT`). `git pull` alone is not sufficient: the service will fail to start with `ModuleNotFoundError: No module named 'bcrypt'` (or `'jwt'`) until the venv is refreshed. Re-run `install.sh`:
v0.6.0 adds native TX support, which requires a one-time HAL recompile and two config files:

```bash
cd /opt/meshpoint
sudo git pull origin main
sudo bash scripts/install.sh
sudo bash /opt/meshpoint/scripts/patch_hal.sh
sudo cp config/sudoers-meshpoint /etc/sudoers.d/meshpoint
sudo chmod 440 /etc/sudoers.d/meshpoint
sudo cp scripts/meshpoint.service /etc/systemd/system/meshpoint.service
sudo systemctl daemon-reload
sudo systemctl restart meshpoint
```

After restart, open `http://<pi-ip>:8080` and you'll be redirected to `/setup` to set an admin password (8-character minimum). All subsequent dashboard access requires sign-in. Forgot the password? `sudo meshpoint reset-password` from SSH.

### Upgrading from v0.6.x or earlier (one-time)

v0.7.0 ships the core modules as Python source instead of pre-compiled `.so` binaries. If your install predates v0.7.0, `git pull` alone is not sufficient: Python's import machinery would prefer the stale `.cpython-*.so` files over the new source and you'd silently keep running v0.6.x code. Re-run `install.sh` after pulling to clean them up:

```bash
cd /opt/meshpoint
sudo git pull origin main
sudo bash scripts/install.sh
sudo systemctl restart meshpoint
```

`install.sh` is idempotent and also subsumes the older v0.6.0-era one-time steps (HAL TX sync word patch, sudoers rule, systemd service install) in the same pass, so this single command covers any path from v0.5.x or v0.6.x up to current. Future updates from v0.7.0 onward go back to plain `git pull` + `restart`.
`patch_hal.sh` patches the concentrator HAL for Meshtastic-compatible TX sync words and recompiles (takes about 2 minutes). The sudoers rule allows the dashboard to restart the service when you change settings. Both only need to run once. Future updates go back to `git pull` + `restart`.

---

Expand Down
16 changes: 1 addition & 15 deletions config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ device:
longitude: 0.0
altitude: 25
hardware_description: "RAK2287 + Raspberry Pi 4"
firmware_version: "0.7.3.1"
firmware_version: "0.7.2"

relay:
enabled: false
Expand Down Expand Up @@ -81,20 +81,6 @@ transmit:
interval_minutes: 180 # 5..1440 min, default 180 (3 hr); 0 = disabled
startup_delay_seconds: 60 # delay before first broadcast on boot

web_auth:
# First-run state: hashes are empty -> dashboard forces /setup.
# After setup, the admin hash is written via the API and the JWT
# secret is auto-generated. Rotating jwt_secret (e.g. via
# `meshpoint reset-password`) invalidates every active session.
admin_password_hash: ""
viewer_password_hash: ""
jwt_secret: ""
jwt_expiry_minutes: 60
allow_read_only: false
lockout_attempts: 5
lockout_cooldown_minutes: 5
session_version: 1

mqtt:
enabled: false
broker: "mqtt.meshtastic.org"
Expand Down
27 changes: 0 additions & 27 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,5 @@
# Changelog

### Unreleased

Queued for the next version bump. Bullets in this section will be folded into the release header (and dated) when the version is cut.

### v0.7.3.1 (May 13, 2026)

Hotfix on top of v0.7.3 the same day. Reported by Willard on Discord ~3h after release: dashboard stuck on "Reconnecting..." with no data after upgrading. Two compounding bugs in the new auth path; a stale browser tab against an auth-required server is enough to trigger both.

- **WS auth close frame now actually reaches the browser.** `src/api/server.py` was calling `await websocket.close(code=4401)` *before* `await websocket.accept()`, which causes Starlette to fail the WebSocket handshake with HTTP 403 instead of completing the handshake and sending a close frame. Browsers translate that to close code `1006` (Abnormal Closure) on the JS side, and our `frontend/js/websocket_client.js` only special-cases `4401` for the redirect-to-/login path -- so unauthenticated WS connections fell through to the generic reconnect loop and stuck forever. Fix: `accept()` first, then `close(code=WS_AUTH_CLOSE_CODE)`. Validated end-to-end on .141 (cookie cleared, dashboard refreshed → bounces to /login as intended).
- **Dashboard root (`/`) now redirects unauthenticated requests to `/login`.** The `StaticFiles(directory=..., html=True)` mount on `/` was serving `index.html` to everyone with no auth check, so a stale browser tab could load the new SPA JS and immediately fight the now-auth-required `/ws`. New explicit `@app.get("/")` route registered ahead of the static mount: 302s unauthenticated requests to `/login` (or `/setup` if no admin password is set yet), serves `index.html` for valid sessions. Static asset paths (`/css`, `/js`, `/assets`, etc.) still fall through to the existing mount.
- **Client defense in depth.** `frontend/js/websocket_client.js` now tracks whether the socket reached the `open` state. If `onclose` fires *without* a prior `onopen`, the handshake failed before the close frame could be delivered. The client probes an auth-gated endpoint (`/api/device/status`); the global 401 interceptor in `app.js` then handles the redirect if it's an auth-shaped failure, while real network blips fall through to the existing reconnect schedule. Belt-and-suspenders coverage so a future server-side regression in the close-code path can't strand users again.
- **Internal:** new `tests/test_dashboard_root_route.py` (unauthenticated GET `/` → 302 to `/login`; GET `/` with no admin password yet → 302 to `/setup`; GET `/` with valid cookie → 200 + index.html). New `tests/test_websocket_auth_close_code.py` asserts the WS handshake completes and the close frame's code is exactly `4401` for both no-cookie and bad-cookie cases (regression for the `accept()`-before-`close()` requirement). 403 tests passing, ruff clean.

### v0.7.3 (May 13, 2026)

Local-dashboard authentication, dashboard branding polish, and the second-leg phantom-node leak fix. Auth lands as a hard requirement on every Meshpoint: existing devices upgrading from v0.7.2 will be prompted to set an admin password the first time the dashboard is opened after the upgrade. Pure-Python where it counts; `install.sh` re-run picks up two new dependencies (`bcrypt`, `PyJWT`).

- **Local dashboard authentication.** First-visit redirects to `/setup`, where you set an admin password (bcrypt-hashed, never stored in plaintext, never logged). Subsequent visits land on `/login`. Sessions are stateless JWTs in an HttpOnly + SameSite=Lax cookie; the JWT secret is auto-generated on the device and persisted to `local.yaml` only when `/setup` completes (a fresh-SD install with no admin password yet leaves `local.yaml` untouched, so the existing setup wizard's "Existing config found" detection still works). All `/api/*` routes, the dashboard pages, and the `/ws` WebSocket are now behind `Depends(require_auth)`; unauthenticated calls return 401 (HTTP) or close code 4401 (WebSocket) and the dashboard JS auto-redirects to `/login?next=...`. Failed-login lockout (`web_auth.lockout_attempts`, default 5; `web_auth.lockout_cooldown_minutes`, default 5) is per-username, in-memory, and surfaces a live countdown on the login page via the `Retry-After` header. Optional viewer role for read-only access via `web_auth.viewer_password_hash` + `web_auth.allow_read_only: true`.
- **`meshpoint reset-password` recovery.** New CLI command for the "I forgot the dashboard password" path. Hashes the new password, rotates `web_auth.jwt_secret`, bumps `web_auth.session_version`, and writes everything to `local.yaml` in one operation: every existing browser session is invalidated and the new credentials work immediately. Run via SSH (`sudo /opt/meshpoint/venv/bin/python -m src.cli.main reset-password <new-password>`); requires no service restart.
- **Sign-out in the topbar.** Door-out icon at the far right of the dashboard header. Clicking it POSTs `/api/auth/logout`, the cookie is cleared, and the browser redirects to `/login`. Hover tints accent-cyan; safe under network failure (still redirects, the global 401 interceptor catches any lingering authenticated call).
- **Auth pages get the radar treatment.** `/setup` and `/login` ship a slowly rotating cyan sweep over a deep-navy radar disc with a live identity strip (device name, firmware version, online dot) so you can confirm you're talking to the right Meshpoint before entering credentials. Same `--bg-primary` / Inter / JetBrains Mono palette as the dashboard, single `auth-` BEM prefix, full reduced-motion support. The radar's blip layer is intentionally unwired in v0.7.3: blips are reserved for real concentrator RX events once a deliberately-public scrubbed feed lands in v0.7.4, rather than ship cosmetic randomness today.
- **Dashboard branding.** Topbar now carries the actual Meshpoint logo (40px, rounded-tile gradient mark) where the placeholder trigram glyph was, plus a 256x256 favicon used on `/`, `/setup`, and `/login`. iOS home-screen icon (`apple-touch-icon`) keeps the rounded-tile mark for nicer bookmark rendering. Establishes `frontend/assets/` as the canonical asset folder.
- **Phantom-node leak: drop `STAT_NO_CRC` and unknown-status packets at the HAL boundary.** v0.7.2 closed the `STAT_CRC_BAD` leg of the leak but `STAT_NO_CRC` packets still flowed into the decoder, where the random bytes after the LoRa header parsed as a "valid" Meshtastic packet and produced a phantom node row in the local SQLite (one packet, no name, no role, never heard again). Fleet diagnostics on a high-traffic Meshpoint (nopemesh, v0.7.2 baseline) measured 4 NO_CRC packets per 30 minutes alongside 108 CRC_BAD and 72 CRC_OK, with the resulting rows accumulating into a 92%-phantom local node table (~72k of ~78k total nodes). `SX1302Wrapper.receive()` now drops `STAT_NO_CRC` with a counted WARNING (`RX NO_CRC if=N sf? bw=? rssi=? snr=? size=? (total NO_CRC: N)`) and additionally drops any packet whose status is neither `CRC_OK`, `CRC_BAD`, nor `NO_CRC` so that future HAL revisions introducing a new status code cannot silently re-open the leak. New `no_crc_count` and `unknown_status_count` properties on the wrapper for observability.
- **Defense in depth: drop Meshtastic headers with `hop_limit > hop_start`.** A Meshtastic packet originates with `hop_limit == hop_start` and decrements `hop_limit` at each relay while `hop_start` stays fixed, so `hop_limit > hop_start` is mathematically impossible for an honestly-originated packet. `MeshtasticDecoder._parse_header()` now returns `None` for that combination so the corrupted bytes never reach the storage layer. Caught two of the five fresh phantoms on the kmax test Pi and four of four on nopemesh in fleet diagnostics, independent of the wrapper-level status filter. Zero false-positive risk by construction.
- **Hardware-validated three ways.** Fresh-SD install (Meshpoint-MNTD-RAKV2 .49) exercises the full `install.sh` path with `bcrypt` + `PyJWT` deps and the bootstrap → `/setup` → `local.yaml`-creation flow end to end. Upgrade from v0.7.2 (Sensecap M1) confirms the existing `local.yaml` is preserved untouched on service start, and the `web_auth` block is appended atomically when the user completes `/setup`. RAK V2 .141 confirms the upgrade-on-top-of-RC path. All three flows green; no phantom rows observed on the upgraded high-traffic device since the no-crc fix landed.
- **Internal:** new `requirements.txt` deps `bcrypt>=4.2.0` and `PyJWT>=2.10.0`. New `src/api/auth/` package: `password_hasher`, `jwt_session`, `lockout_tracker`, `auth_service`, `auth_bootstrap`, `dependencies`, `ws_guard`. New routes `src/api/routes/auth_routes.py` and `src/api/routes/identity_routes.py`. New CLI `src/cli/reset_password_command.py`. New frontend `frontend/auth/{setup,login}.html`, `frontend/css/auth.css`, `frontend/js/auth.js`, `frontend/js/signout_controller.js`. New tests: `test_password_hasher`, `test_jwt_session`, `test_lockout_tracker`, `test_auth_service`, `test_auth_dependencies`, `test_auth_routes`, `test_auth_page_serving`, `test_auth_bootstrap`, `test_identity_route`, `test_protected_router_wiring`, `test_reset_password_command`, plus the no-crc test additions in `test_sx1302_wrapper.py` and the new `test_meshtastic_decoder_header_validity.py`. `tests/test_relay_node_header.py` default `flags` byte moved from `0x03` (hop_limit=3, hop_start=0, structurally impossible) to `0x63` (hop_limit=3, hop_start=3, valid direct packet) so existing relay-node tests pass under the new validity check. 401 tests passing, ruff clean, bandit clean.

### v0.7.2 (May 5, 2026)

Two-fix bundle on top of v0.7.1. One small UX feature for hop-chain debugging, one quiet-but-important correctness fix that was inflating node counts on the cloud catalog and producing intermittent garbled-but-readable text on the local mesh. Both ride together because they touch the same RX path. Pure-Python, no recompile needed.
Expand Down
Loading