Commit Graph

595 Commits

Author SHA1 Message Date
MarekWo
a2d3111e1c feat(channels): sort sidebar by latest activity, with favorite tier
Channels in the sidebar and mobile dropdown now sort by most recent
message first, with favorited channels pinned above non-favorites.
Reordering is push-driven via the existing new_message socket event:
the affected item is moved to the top of its tier in the DOM, no full
re-render. Favorites are toggled via a star icon in Manage Channels
and persisted in read_status.is_favorite for cross-device sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 22:48:33 +02:00
MarekWo
a335f521e4 docs: cover regions, searchable channel selector, path hash mode
User-guide: new Region Scopes section (registry CRUD, per-channel picker,
firmware v1.15 default), updated Switching Channels (searchable picker on
narrow screens, sidebar previews on wide), Settings Regions tab, path_hash_mode
in Device tab.

Architecture: regions/channel_scopes tables, /api/regions and /api/channels/scopes
endpoints, per-channel scope-key push under _send_lock in DeviceManager,
path_hash_mode field in /api/device/config, channel POST/join idempotency.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 07:45:55 +02:00
MarekWo
0d4e81d105 feat(regions): show clickable No region badge when channel has no scope
The status-bar pill used to disappear when the active channel had no
region scope, forcing users into Manage Channels to assign one. Now it
stays visible as a muted "No region" badge that opens the same Set
Region Scope picker when clicked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 20:57:57 +02:00
MarekWo
da82a46591 fix(regions): use explicit None entry to clear default in Region Registry
Replace the click-the-selected-radio-again gesture with a top-row
"None — use firmware default" radio, mirroring the per-channel region
picker. Users found the toggle gesture unintuitive; an explicit option
matches the picker pattern they already know.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 20:18:44 +02:00
MarekWo
d858011228 fix(regions): reset backdrop inline z-index on hide so picker stays clickable
Bootstrap reuses the same .modal-backdrop element across show/hide cycles.
The previous stacked-modal fix bumped its z-index to 1065 inline but never
cleaned it up, so the next non-stacked open of the picker (e.g. via the
status-bar badge) reused that 1065 backdrop above the default-1055 modal,
covering the entire viewport with an unclickable overlay.

Capture the bumped backdrop reference in onShown and clear its inline
z-index in onHidden alongside the modal's.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 11:42:09 +02:00
MarekWo
4bf863ec27 fix(channels): show full year in channel-list date to avoid HH.MM lookalikes
Old DD.MM rendering (e.g. "20.04") was visually indistinguishable from a
time stamp; switch to DD.MM.YYYY for messages older than today.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 22:03:27 +02:00
MarekWo
05291e88fb fix(regions): dim Manage Channels modal when region picker opens on top
Bump the picker modal's z-index (1075) and its backdrop (1065) above the
underlying Manage Channels modal (1055) so the two layers visually
separate instead of blending together.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 21:55:14 +02:00
MarekWo
d77d86087d fix(regions): allow clearing the default region in Region Registry
Click the already-selected radio to clear the default; new
DELETE /api/regions/default endpoint also pushes an empty CMD 63 to
the firmware so its persistent default is wiped too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 21:39:17 +02:00
MarekWo
e293de2a76 fix(regions): rename tab to Regions and soften v1.14 firmware error
Two small follow-ups after initial deployment.

- Rename the Settings tab 'Channels' -> 'Regions' (id now tabSettingsRegions).
  The tab manages the region registry, not channels; the old label was
  confusing. The per-channel picker still lives under Manage Channels as
  before.
- Graceful handling of firmware rejection: CMD_SET_DEFAULT_FLOOD_SCOPE
  (63) and CMD_GET_DEFAULT_FLOOD_SCOPE (64) were introduced in firmware
  v1.15.0; on v1.14.x the device replies with a generic ERR frame and
  our toast showed the unhelpful 'Firmware error: unknown'. Now the
  device_manager translates the empty/timeout reason into a concrete
  message naming the v1.15 requirement, and the api handler appends
  'Your choice is saved locally' so the user knows the local state
  still persists. Same treatment for the delete-default-region clear
  path.
2026-04-24 11:54:01 +02:00
MarekWo
226bd2abac feat(regions): status-bar indicator pill for active channel scope
Final slice — small but completes the feature.

- index.html: add #regionIndicator pill to the chat status bar, inline
  with the connection-status dot. Hidden by default; click opens the
  region picker for the current channel.
- app.js: loadChannelScopes() fetches /api/channels/scopes at page init
  (right after loadChannels). updateRegionIndicator() toggles the pill
  based on currentChannelIdx + window.channelScopes and is called on
  every successful loadMessages + after saveChannelScope.
- DM view deliberately untouched — region scope applies to flooded
  channel sends only, not DMs.
2026-04-24 07:29:12 +02:00
MarekWo
afe0c7cf17 feat(regions): per-channel scope picker + send-flow integration
Fourth slice — the feature is now functional end-to-end from UI to radio.

- Manage Channels modal: each row now has a pin-map button between Mute
  and Share that opens a region picker for that channel; rows show an
  inline badge with the assigned region name.
- Region picker modal (new #regionPickerModal): radio list of regions
  with a "(None) — use firmware default" option at the top. Empty-state
  shows a "Manage Regions" CTA that deep-links to Settings > Channels.
- api.py: two new routes —
  - GET /api/channels/scopes          → bulk map for UI rendering
  - PUT /api/channels/<idx>/scope     → {region_id: int | null} set/clear
- device_manager.send_channel_message: looks up the channel's scope,
  then — under _send_lock — pushes the 16-byte key via CMD 54 before
  the actual send_chan_msg. Channels without a mapping get an all-zero
  key so a previously-set scope doesn't leak across channels (firmware's
  send_scope is sticky until overwritten, not one-shot).
2026-04-24 07:27:33 +02:00
MarekWo
f04f0f1dd8 feat(regions): Settings > Channels tab with region registry CRUD
Third slice — users can now curate their device-wide region list. No
per-channel mapping yet; that's PR #4.

- base.html: new Channels tab in the Settings modal with an info banner
  pointing at regions.meshcore.nz, the list container, and an add-region
  form.
- app.js: loadRegions / addRegion / deleteRegion / setDefaultRegion
  mirroring the loadContactsSettings / saveContactsSetting pattern. Client
  -side name validation (isValidRegionName) mirrors the firmware
  RegionMap::is_name_char byte-rule exactly so users get instant feedback
  on invalid chars without a round-trip.
- api.py: four routes under /api/regions —
  - GET    /api/regions                       → list registry
  - POST   /api/regions   {name}              → derive key + insert; 409 dup
  - DELETE /api/regions/<id>                  → cascade channel mappings; if
      the deleted region was firmware default, best-effort clear on device
  - POST   /api/regions/<id>/default          → flip DB flag + push CMD 63;
      if firmware push fails, DB still flips and response includes a
      non-blocking `warning` for a toast
2026-04-24 07:24:15 +02:00
MarekWo
0e38e0ce8c feat(regions): DeviceManager wrappers for flood-scope commands
Second slice of the per-channel region-scope feature — firmware plumbing.
No UI, routes, or send-flow integration yet; those land in PR #3 / #4.

- _send_lock: threading.Lock added to __init__ (consumed in PR #4 to
  serialize the set-scope + send-channel-message pair across Flask
  threads; introduced here to keep the init diff small).
- set_flood_scope_key(key_hex): thin wrapper over the existing
  meshcore-py `set_flood_scope(bytes)` path (CMD 54). None/empty clears
  the volatile scope. Used on the channel-send hot path in PR #4.
- set_default_flood_scope(name, key_hex): hand-rolled CMD 63 frame
  (opcode + 31-byte NUL-padded name + 16-byte key = 48 bytes) via the
  lib's generic send() with [OK, ERROR] wait. Installed meshcore-py
  (<=2.2.15) has no wrapper for this opcode; frame format matches
  MyMesh.cpp lines 1893-1909.
- Deliberately NOT implementing CMD 64 (GET_DEFAULT_FLOOD_SCOPE): the
  library's reader drops RESP_CODE 28 as "unhandled" (reader.py:919-921),
  so there is no Event we can wait for. Until upstream adds support,
  mc-webui treats its own regions.is_default row as the source of truth
  and pushes one-way via CMD 63. Comment in code documents the reason.
2026-04-24 07:20:30 +02:00
MarekWo
8e353407d3 feat(regions): add data layer for per-channel region scopes
Introduces the SQLite-backed region registry and channel->region mapping
that will drive the per-channel flood-scope feature. No UI or device
wiring yet; those land in subsequent PRs.

- schema.sql: new `regions` and `channel_scopes` tables + partial index
  on the default flag.
- database.py: CRUD helpers for regions (create/list/get/delete/default)
  and channel_scopes (set/get/bulk-load) with ON DELETE CASCADE.
- app/meshcore/regions.py: pure helpers for SHA256('#'+name)[:16] key
  derivation and firmware-compatible name validation (mirrors the
  `RegionMap::is_name_char` rule `c in {-,$,#} or c>='0' or c>='A'`).
- tests/test_regions.py: known SHA256 vectors, validator coverage
  (incl. the firmware quirk that `_` and other 0x5B-0x60 chars are
  admitted), and CRUD + cascade integration tests.
2026-04-24 07:12:55 +02:00
MarekWo
daf9c5c0db feat: show last message time and preview in channel list
Each channel item in the desktop sidebar and the mobile dropdown now
surfaces the timestamp of the last message (HH:MM today / DD.MM older)
and a truncated plain-text preview (up to 60 chars, mentions stripped).
Sidebar clamps preview to 2 lines, dropdown to 1 line. Empty channels
render as a single-line name, unchanged.

- api.py: /api/messages/updates returns last_message_preview +
  last_message_time; new _make_preview helper strips @[name] syntax
  and truncates with ellipsis.
- app.js: new channelLastMessages state populated by the poll loop and
  by the new_message socket event; populateChannelSidebar,
  renderChannelDropdownItems, and updateChannelSidebarBadges build and
  maintain the two-row layout (.channel-item-top + .channel-item-preview).
- style.css: sidebar and dropdown items switch to column flex; new
  .channel-item-top, .channel-last-time, .channel-item-preview rules.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-23 07:43:02 +02:00
MarekWo
57a0ca018d fix: treat slots with empty name as empty regardless of secret bytes
Some firmwares return SHA256(\"\")[:16] (e3b0c442...) for an empty
channel slot's secret instead of all zeros. The load path checked only
for the all-zero sentinel, so those slots passed the \"valid\" branch
and got persisted to the DB with a synthetic 'Channel N' name plus the
bogus secret. The stale rows then leaked into db.get_channels() and
would have supplied wrong keys for pkt_payload computation.

Anchor the decision on name presence: a slot is used iff firmware
returned a non-empty name. Drop the 'Channel {idx}' fallback so we
never invent names for empty slots. The existing end-of-loop cleanup
then removes any phantom rows already in the DB on next connect.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 20:52:35 +02:00
MarekWo
cbcdbdcae9 fix: prevent duplicate channels from concurrent add/join requests
Two near-simultaneous POSTs to /api/channels/join (observed 7 ms apart
in demo-server logs) each found a different free slot and both
succeeded, producing two entries for the same channel name on the
device. This also shifted the sidebar so each channel rendered the
next one's messages.

- Wrap free-slot detection + set_channel in a module-level lock so
  concurrent requests serialize instead of racing.
- Idempotency: if a channel with this name already exists, return the
  existing slot with already_existed=true instead of creating a
  duplicate. Applies to both POST /api/channels and /api/channels/join
  (skipped when caller targets an explicit index).
- Disable submit buttons on create/join forms while a request is in
  flight, and guard against double-registration of the channel-link
  click delegate to stop a single click from firing N POSTs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 20:45:39 +02:00
MarekWo
66c378c17d fix: shrink navbar brand to 1rem on mobile so full 'mc-webui' fits
At 1.25rem the brand was still ~120px wide and got truncated to
'mc-we...' on a 360px-wide S20. Drop to 1rem so the whole name
fits without ellipsis while the navbar stays on one row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 20:00:18 +02:00
MarekWo
10addba35f fix: force navbar to one row on mobile (flex-wrap: nowrap)
The actual root cause: Bootstrap's .navbar defaults to flex-wrap: wrap,
which lets the brand and the controls drop to separate rows when the
total just barely overflows. Adding flex-wrap: nowrap on .navbar and
its .container-fluid (which inherits flex-wrap) keeps everything on
one row. Brand also gets min-width: 0 + overflow: hidden +
text-overflow: ellipsis so it truncates gracefully if there's
genuinely no room (instead of forcing overflow).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 19:55:23 +02:00
MarekWo
14df7ead4d fix: shrink navbar brand on mobile so navbar controls fit one row
The selector was already narrow enough — the real culprit was the
.navbar-brand.h1 sitting at Bootstrap's default 2.5rem (40px), which
on a Samsung S20 ate ~200px just for 'mc-webui'. Cap brand at
1.25rem on screens ≤ 768px so the bell + selector + menu can sit
beside it on one row.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 19:50:12 +02:00
MarekWo
9ca055dd01 fix: drop input min-width override that pinned channel selector to 100px
The responsive @media (<768px) block had #channelSelectorInput pinned
at min-width: 100px !important, which pushed the wrapper's actual width
above the wrapper's own min-width: 80px. Remove the input min-width so
the wrapper's 80px takes effect; DM contact input keeps its 100px.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 19:44:48 +02:00
MarekWo
b296913cd1 fix: shrink channel selector further to fit Samsung S20 navbar in one row
Reduce wrapper min-width 100px → 80px and tighten the form-select
chevron padding (2rem → 1.5rem right, 0.6rem → 0.5rem left) so the
navbar fits in one row on ~360px-wide phones while keeping the
chevron and text both visible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 08:46:19 +02:00
MarekWo
8194158241 fix: narrow channel selector to keep navbar on one line on small screens
Reduce wrapper min-width from 140px to 100px (with 140px max-width) so
the bell + channel selector + menu button all fit in one navbar row on
~412px-wide phones, without sacrificing readable channel names when
there's room to grow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 08:42:16 +02:00
MarekWo
309efe0ce5 feat: searchable channel picker on narrow screens + DM picker font fix
Replace the native <select> channel picker (used on narrow screens) with
a custom searchable dropdown matching the DM contact picker UX: type to
filter, arrow/Enter keyboard nav, click-outside to close, per-channel
unread badges, muted styling. Wide-screen sidebar (lg+) is unchanged.

Also align the DM picker dropdown font with the wide-screen DM sidebar
(0.88rem / weight 400) — was inheriting larger/bolder from form-control.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 08:32:03 +02:00
MarekWo
3b4ed26c50 feat: path_hash_mode selector in Settings + global Close button
Adds a Path hash mode dropdown (1B/2B/3B) to Settings → Device → Public
Info, so the mode can be switched from the UI instead of the meshcli
console. The Settings modal now has a persistent Close button in the
footer, visible on every tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 07:32:50 +02:00
MarekWo
cfdb68390f docs: add Contact Management guide (markdown + HTML)
Introduces a dedicated conceptual walkthrough of device contacts, the
cache layer, the ignored/blocked flags, Settings → Contacts toggles,
recommended configuration, scenarios, recovery from the 350 limit,
auto-cleanup interaction, and FAQ/migration notes for users coming
from the official Android/iOS apps.

Also ships a standalone, responsive HTML version with embedded CSS
(light/dark auto) for sharing outside the repo, and links the guide
from user-guide.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 11:21:39 +02:00
MarekWo
3dd1c52687 feat: contacts settings tab with suppress + auto-ignore options
Move Manual approval toggle into a new Contacts tab in the global
Settings modal and clean up the Contact Management panel (drop the
duplicated Settings/Manage Contacts headers, shorten the Existing
Contacts blurb). Add two new persisted options gated on Manual
approval being ON: Suppress new advert notifications (frontend hides
FAB badge + browser notification while the Pending list itself stays
populated) and Automatically add new contacts to "Ignored" (advert
handler marks the new contact ignored before emitting pending_contact,
so the user is silenced end-to-end while contacts remain in the cache
for promotion via "To Device").

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 10:01:58 +02:00
MarekWo
0a08759065 docs: sync README, user guide, and architecture with recent features
Catch up on ~36 commits since b60c99a. Document Device Settings tab
(public info + radio) with map picker and regional presets, quick-access
FAB cluster with drag/collapse/sizing, configurable route popup and toast
timeout/position, multi-arch Docker images (amd64, arm64, arm/v7), and
new endpoints /api/device/config and /api/ui/settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 16:14:57 +02:00
MarekWo
8ccb3100c2 fix: cascade-clean ignored/blocked rows on hard contact delete
hard_delete_contact() failed with FOREIGN KEY constraint when the
contact had a row in ignored_contacts or blocked_contacts, since those
FKs lacked ON DELETE CASCADE. Delete dependent rows first in the same
transaction; also update schema for new deployments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 08:35:31 +02:00
MarekWo
bd0a6b492e feat: configurable route popup + toast display time and position
Users complained that the route popup under group-chat messages and the
top-of-page notification toasts auto-close before they can read them, and
some users wanted to move the toasts out of the top-left corner.

Adds to Settings modal:
- Group Chat tab: route popup auto-close timeout + "don't close" switch
  (applies to both channel popups and DM route popups)
- New Interface tab: toast auto-close timeout, "don't close" switch, and
  five position options (top-left/top-right/bottom-left/bottom-right/center)

Persisted as chat_settings (extended) and a new ui_settings row in the
app_settings table, with /api/chat/settings and /api/ui/settings endpoints.
Default toast delay bumped from 1.5s to 2s.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 08:12:51 +02:00
MarekWo
77c3ffa5c2 fix: prevent echo mis-correlation for sent channel messages
Pre-compute expected pkt_payloads at send time using channel secret +
timestamp (±3s for clock drift), then match echoes exactly instead of
only checking the 1-byte channel hash. Fixes race condition where an
incoming message's echo on the same channel could be incorrectly
attributed to a just-sent message (wrong Analyzer URL).

Falls back to channel-hash matching when channel secret is unavailable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-14 21:47:07 +02:00
MarekWo
10cc2031d1 feat: add GitHub Actions cache for Docker layer builds
Use GHA cache backend (cache-from/cache-to) so that unchanged
layers (especially the slow ARM pip install) are reused across runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 21:30:05 +02:00
MarekWo
74e4327a37 fix: add build deps for Pillow/pycryptodome on ARM
gcc, python3-dev, libjpeg-dev, zlib1g-dev are needed to compile
Pillow and pycryptodome from source on linux/arm/v7 (no pre-built
wheels available). Build deps are purged after pip install.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 21:15:31 +02:00
MarekWo
d04fd817f9 feat: add multi-arch Docker builds for Raspberry Pi support
Add QEMU and Buildx to CI workflow to build images for linux/amd64,
linux/arm64, and linux/arm/v7 platforms. Closes #29 discussion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 21:06:27 +02:00
MarekWo
f7f5beb8b8 fix: route popup positioning on small screens + tap-to-copy
Right-align path popup and cap max-width to viewport to prevent
overflow on narrow screens (same approach as DM route popup fix).
Add tap-to-copy on route entries — copies path in comma-separated
format (e.g. 5E,32,0D,8C) to clipboard with visual feedback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 20:53:54 +02:00
MarekWo
acec9e92cf fix: use calendar date comparison for message timestamps
The old code in app.js used elapsed-time division to determine
"today" vs "yesterday", causing messages from late evening to
show as "today" when viewed shortly after midnight. Now both
app.js and dm.js compare calendar dates via toDateString().
Also adds "Yesterday" label support to dm.js.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-12 13:19:10 +02:00
MarekWo
8f8bd30747 fix: refresh mc.contacts from device on dirty flag to update stale names
Contact names stayed stale indefinitely because mc.contacts (in-memory
dict) was only populated at startup. When a remote node renamed itself,
the device firmware updated its contact list but the app never re-read it.

Now ensure_contacts(follow=True) is called when contacts_dirty is set:
- In _on_advertisement(): refresh before name lookup (incremental via lastmod)
- In get_contacts_with_last_seen(): refresh + DB sync before serving API data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 12:29:45 +02:00
MarekWo
bbfca38d34 fix: use adv_lat/adv_lon keys for device coordinates
Device info from meshcore uses adv_lat/adv_lon, not lat/lon.
Fixed in get_param, set_param (lat/lon individually), and the new
/api/device/config endpoint.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 19:26:42 +02:00
MarekWo
58d7d9af18 feat: add Device settings tab with Public Info and Radio Settings sub-tabs
Add a Device tab as the first tab in the Settings modal with two sub-tabs:
- Public Info: device name, coordinates with map picker, advert location sharing
- Radio Settings: frequency, bandwidth, SF, CR, TX power with region presets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 17:08:57 +02:00
MarekWo
bc1da9e45e fix: get_device_info checked for 'data' attr instead of 'payload'
Event objects use 'payload', not 'data'. This bug was latent because
the cache was always populated during connect — only exposed after
the cache invalidation fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 14:43:26 +02:00
MarekWo
1e6f8caf03 fix: invalidate self_info cache after set_param
get_device_info() cached SELF_INFO payload in _self_info and never
refreshed it after set operations, so get always returned stale values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 14:40:14 +02:00
MarekWo
c3f61ce3f7 fix: get radio returns actual values, implement set radio command
get radio used wrong key names (freq/bw/sf/cr instead of
radio_freq/radio_bw/radio_sf/radio_cr from SELF_INFO payload).

set radio was missing entirely — would silently fall through to
custom variable handler. Now parses freq,bw,sf,cr and calls
mc.commands.set_radio().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 14:33:13 +02:00
MarekWo
3086ac1f8e fix: add value hints to console help for telemetry, advert and path params
Unified format: <0-2> on the left, (0=x/1=y/2=z) on the right.
Values sourced from MeshCore firmware defines:
- telemetry_mode_*: 0=off, 1=selected, 2=all
- advert_loc_policy: 0=none, 1=share, 2=prefs
- path_hash_mode: 0, 1, 2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 14:24:25 +02:00
MarekWo
1a194d5050 fix: implement get advert_loc_policy console command
The set command was implemented but get was missing, causing
"Unknown param" error. Reads adv_loc_policy from device info.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 14:10:20 +02:00
MarekWo
19b2a172c8 feat: persist FAB collapsed state across page loads
Save collapsed/expanded state to localStorage (shared key for both
main chat and DM views) so buttons stay hidden when the user
previously collapsed them.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 12:26:56 +02:00
MarekWo
fb99054e4b fix: defer FAB position restore when iframe viewport is too small
The DM iframe reloads on every modal open. During the show transition
the viewport is 0x0, causing clampFabPosition to push buttons to the
top-left corner. Now polls until viewport is valid before restoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 12:19:44 +02:00
MarekWo
60c698deb2 feat: add Settings FAB button, drag-and-drop positioning, and size/spacing controls
- Add Settings quick-access button to both main chat and DM views
- Make FAB container draggable via toggle button with position saved to localStorage
- Add button size and spacing sliders in Settings > Appearance tab
- Use CSS custom properties for dynamic FAB sizing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 11:59:24 +02:00
MarekWo
6c02220719 fix: skip empty channel slots during sync, clean up stale DB channels
Empty device channel slots have all-zero secrets (32 hex chars) which
passed the length check and got persisted to DB as "Channel N". This
caused ghost channels (e.g. Channel 14) to appear in unread counts
while the sidebar correctly showed only real channels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 11:08:38 +02:00
MarekWo
c36d7b5fbf fix(ble): simplify reconnection — rely on container restart for clean state
In-container BLE reconnection is unreliable because bleak leaves stale
GATT notification handles after abnormal disconnect, and adapter power-
cycling from within Docker corrupts bleak's internal BlueZ manager state.

New approach:
- On BLE disconnect or keepalive failure, immediately mark as permanently
  failed (no in-container reconnect attempts)
- Health endpoint returns 503, Docker healthcheck triggers container restart
- Docker entrypoint script disconnects stale BLE connections before app
  starts, ensuring clean GATT state for bleak

This is reliable because:
- MeshCore.create_ble(address=...) works on fresh container starts
- The BlueZ daemon on the host maintains adapter state correctly
- Container restart is fast (~5s) and gives a truly clean BLE state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 16:39:03 +02:00
MarekWo
53063f199a fix(ble): connect via BlueZ D-Bus instead of bleak direct connect
bleak inside Docker cannot initiate new BLE connections — it can only
take over connections already established by BlueZ.  Replace the
force-disconnect approach with a connect-via-BlueZ approach:

1. _ble_ensure_connected() connects the device via BlueZ D-Bus
   (Device1.Connect) before bleak tries to take over
2. BleakScanner.find_device_by_address() provides the BLEDevice
   object that bleak 3.x needs (raw MAC address doesn't work)
3. MeshCore.create_ble(device=...) takes over the BlueZ connection

On reconnect after disconnect:
1. Power-cycle adapter clears stale GATT notification handles
2. BlueZ re-connects the trusted device automatically
3. bleak takes over the re-established connection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 14:13:06 +02:00