183 Commits

Author SHA1 Message Date
MarekWo 4c6857a00f feat: numbered releases (2.1.0) with git tags and GitHub releases
The app already knew exactly which build was running - a calendar version
of commit date plus short hash - but nothing gave a release a name users
could quote, and the repo had no tags at all.

Adds a VERSION file as the single source of truth for a SemVer release
number, read by app/version.py alongside the existing build string rather
than replacing it: the number is for people, the build is for pinning down
a deploy, and both ship in /api/version and the template context. The menu
shows the release first with the build underneath. #versionText still holds
the build string, because the remote-update poller compares it to detect
that the server came back on a new build.

Resolution order is unchanged (frozen file > git > fallback), and a frozen
file written before this change still yields a correct release number, so
an already-deployed server does not need re-freezing to stay sane. The
container has no git, hence COPY VERSION into the image - otherwise a plain
'docker compose build' reports 0.0.0.

scripts/release.sh cuts a release from main: it refuses a dirty tree, a
wrong branch, a malformed number or an existing tag, extracts the notes
from the matching whatsnew section, then tags, pushes and publishes via gh.

Numbering starts at 2.1.0 rather than 1.x: the v2 line has been in
production since March and 'v1' is the archived pre-migration branch, so
1.x would have been ambiguous. Sections in whatsnew before 2.1.0 keep
their date-only headings - they were never tagged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 19:20:57 +02:00
MarekWo f31e4e160c feat(pathanalyzer): map overlay toggles for repeaters and alternative paths
The map used to plot every located repeater on top of the selected route,
which buried the path the user actually asked for. Both extras are now
opt-in, via a Leaflet control in the map corner - the shared filter bar
would be the wrong place, since these apply to the Map view only.

- "All repeaters": the purple contact markers, off by default (also saves
  plotting ~650 markers on every render)
- "Alternative paths": the selected message's other echoes, each in its
  own light hue, keyed to a swatch on the matching sidebar row

Echoes of one message usually share a long prefix, so a segment is drawn
once only - the primary route claims it, and an alternative then shows
exactly where it diverges instead of hiding underneath. Each alternative
also gets a dot at its last resolved hop, since the difference is often
just the final one.

Toggling re-renders without refitting the bounds, so it never throws away
the viewport the user panned or zoomed to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 18:35:06 +02:00
MarekWo d8d3427dc9 feat(pathanalyzer): chat route click opens the analyzer map deep-linked
Clicking a route in the chat path popup now opens Path Analyzer on the
Map view with that message selected and that exact echo path drawn,
instead of copying the route to the clipboard. Copy stays available as
a small per-route clipboard icon in the popup.

Deep link flow: popup click stores {packet_hash, path hex} in
window.paDeepLink; the modal show handler builds the iframe URL with
?hash=&path=; the analyzer resolves the message after load (widening
the time range once to 7 days if needed), matches the echo by raw path
hex (fallback: shortest), and switches to the map. A plain menu open
still loads the analyzer without any deep link.

Verified live via Playwright: clicked the 3rd (non-shortest) route of a
multi-route message - the map opened with exactly that echo selected,
including the 3-to-7-day widening retry (message was 4 days old).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 07:10:31 +02:00
MarekWo afb305a61e feat(pathanalyzer): Routes view - hop segment statistics + sequence filter
New 4th view counting consecutive hop segments (n-grams) across all
routed echo paths, regardless of position in the path:
- segment length selector (2/3/4 hops), table sorted by echo count by
  default; columns: Echoes, distinct Messages, As path end (how often
  the segment is the final part of the path reaching us)
- resolved contact names shown under the hash chips (ambiguous/unknown
  marked like elsewhere)
- row click fills the repeater filter with the segment and jumps to the
  message list
- the repeater filter now accepts consecutive sequences chained with
  > or an arrow (e.g. AFE6>6E9A or hash>name); single values behave
  exactly as before, spaces stay usable inside contact names
- view switcher buttons show icons only on xs screens so 4 buttons fit

Idea by Daniel - group paths by recurring hop sequences to see which
routes carry the most traffic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:38:47 +02:00
MarekWo 0d9d9ecd57 feat(pathanalyzer): map view polish - channel label, auto path, 45vh map
- Show the channel name (dimmed, next to the sender) on map-view tiles
- Clicking a message now auto-selects and draws its shortest routed echo
  (fewest hops, ties -> first); re-clicking keeps the user's echo choice
- Mobile: map takes a fixed 45% of the viewport height and the path list
  gets the remaining space (was min 300px map / max 40% list)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:08:19 +02:00
MarekWo 944493a56e feat(pathanalyzer): collapsible filter panel on small screens
On viewports below 768px the toolbar collapses to a single line (view
switcher + Filters toggle + counter); filters expand on demand and an
active-filter count badge shows on the toggle when collapsed. Desktop
layout is unchanged (the panel renders with display: contents). Frees
most of the vertical space for the path list on the mobile Map view.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 20:50:11 +02:00
MarekWo f54dc1d481 feat(repeaters): prefill saved password on login retry, map location picker, tidy list
- Login retry prompt now prefills the saved password (a wrong stored
  password and an unreachable repeater are indistinguishable, so a
  connection-caused failure no longer forces retyping). Adds
  GET /api/repeaters/<pk>/password for the trusted local UI.
- Repeater list: "last login" moves to its own line so a long path keeps
  the full row width on narrow phones.
- Settings -> Location: "Pick from map" button opens a Leaflet picker;
  clicking the map fills lat/lon and marks the section dirty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:27:13 +02:00
MarekWo 19c35f4a2b feat(pathanalyzer): combined 2/3-byte option in the HB filter
The path-hash-size filter accepts a comma list of sizes; a new
'2/3-byte' option matches messages with any routed echo using a 2- or
3-byte hash.

Verified live: 2/3-byte returns exactly the union of the 2-byte and
3-byte sets (220 = 208 + 12 with zero overlap misses on live data).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 12:14:50 +02:00
MarekWo c1853fe8d7 feat(pathanalyzer): map path styling and manual-pick undo
- Drawn paths now use red (#dc3545) for hop markers and polylines,
  clearly distinct from the purple base repeater dots (origin stays
  green, ambiguous candidates amber)
- Resolved hops render as numbered badges matching the legend order,
  with a permanent name label next to each point (origin included)
- Manual candidate assignments are reversible: a picked hop shows an
  undo icon in the legend, and a 'Reset picks' button clears every
  manual assignment on the current path

Verified live via Playwright: line/marker colors, badge numbers and
name labels, pick -> per-hop undo -> re-pick -> reset-all flow, no
page errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 11:46:32 +02:00
MarekWo 0ad26a66f8 fix(pathanalyzer): Messages and Repeaters views fit narrow screens
Rather than a card-per-row transform (too much scrolling for 400+
messages), narrow viewports (<=576px) keep the table but shed
secondary width:
- Hash, HB and Echoes columns hidden; packet hash + HB surface as a
  line at the top of the expanded detail row instead (still copyable)
- time renders as short date/time stacked on two lines (full timestamp
  on wide screens)
- sender/channel/message-preview columns capped with ellipsis, tighter
  cell padding, headers allowed to wrap
- stats view: Messages and As-last-hop columns hidden on phones
  (Repeater, Contact, Relayed, Avg SNR remain), contact names truncated
- expanded echo paths now wrap fully within the viewport, keeping every
  hop chip and the jump-to-map button reachable

Root cause of the first attempt failing: the media block sat mid-
stylesheet, so later equal-specificity base rules overrode it - it now
sits last, with a comment pinning it there.

Verified live via Playwright at 390px: zero horizontal overflow in
Messages (incl. an expanded 20-hop path, map button on-screen) and
Repeaters; desktop 1280px layout unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 21:53:57 +02:00
MarekWo 0ba0503547 feat(pathanalyzer): repeater filter matches by name as well as hash
The repeater filter input now accepts either form: pure-hex input still
prefix-matches hop hash tokens, and any input also matches the names of
contacts each token resolves to (pubkey-prefix candidates, memoized per
token). With 1-byte hashes a name can match via a colliding candidate,
so the filter is intentionally inclusive: it keeps messages where the
named repeater could be on the path.

Verified live via Playwright: hex prefix regression OK, name search
returns only messages with a token resolving to the named contact,
memoized pass over 606 messages takes <1 ms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:44:32 +02:00
MarekWo 3522dbabef feat(pathanalyzer): message text filter
Free-text case-insensitive substring filter on message content, next to
the other toolbar filters; combines with them and is reset by Clear.

Verified live via Playwright: content match returns only matching rows
(5/606), combines with the sender filter, Clear restores the full set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:42:42 +02:00
MarekWo 9574a7ebee feat(pathanalyzer): jump-to-map from echo rows, message preview in map sidebar
- Each routed echo line in the Messages table gets a map icon that
  switches to the Map view with that message + echo pre-selected and
  its path drawn; the sidebar scrolls the selection into view
- Map sidebar entries show the message content under the sender: one
  truncated line normally, full text when the message is selected;
  sidebar entry markup switched from innerHTML to DOM building so
  network-supplied sender/content strings are never parsed as HTML
- Fix: path drawing (fitBounds) now runs after invalidateSize in the
  same deferred step - drawing immediately after unhiding the map
  container computed bounds against a stale size and left the map at
  world zoom when jumping from the table

Verified live via Playwright: jump selects the right echo (route
chips match the sidebar entry), path draws at zoom 10, previews
truncate on unselected entries and expand on the selected one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:36:11 +02:00
MarekWo 67b405251b feat(pathanalyzer): path hash size (HB) column and filter
- HB column in the message table: distinct hash sizes (bytes per hop)
  across the message's routed echoes, e.g. '1' or '1,2'; em dash when
  no routed echo
- HB filter (Any/1-byte/2-byte/3-byte): matches when any routed echo
  uses that hash size; 0-hop echoes are excluded since their stored
  hash_size is meaningless; combines with the other filters and is
  reset by Clear

Verified live via Playwright: 1B/2B/3B filters each return only
messages with a matching routed echo (183/166/9 of 606, zero false
positives), combined hops+HB filtering works, Clear resets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:19:00 +02:00
MarekWo dca3d58232 feat(pathanalyzer): map view with path drawing (stage 5)
- Third view toggle: Map. Leaflet lazy-init on first activation with
  invalidateSize() once the container is visible; OSM tiles as in the
  main app map
- Base layer: repeater contacts with geo plotted as purple circle
  markers (name + pubkey prefix popup), from /api/contacts/cached
- Sidebar lists filtered messages that have routed echoes; click a
  message to see its echoes, click an echo to draw it
- Hop resolution by pubkey prefix: single geo candidate draws a solid
  hop point; collisions render all candidates as amber markers plus a
  legend row with clickable pick-chips (pick resolves the hop and
  redraws); unknown hops are listed as 'unknown / no position'
- Polyline follows path order with the sender (name-matched contact) as
  origin when available; segments bridging unresolved hops are dashed;
  map fits bounds to the drawn path
- Clear button erases the drawn path; stale selections are dropped when
  filters change underneath

Verified live via Playwright: 252 repeater markers, path + legend
render, ambiguous-hop candidate pick adds a polyline segment, clear
empties the layer, view toggling stays stable with no page errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:15:59 +02:00
MarekWo ca24d62ca6 feat(pathanalyzer): per-repeater statistics (stage 4)
- Messages / Repeaters view toggle in the toolbar; stats aggregate over
  the currently filtered messages so both views share one filter state
- Per exact hash token: Relayed (echoes containing the token anywhere),
  distinct Messages, As-last-hop count, and Avg SNR counted only over
  echoes where the token is the final hop (SNR is measured at our
  receiver, so it is never attributed to intermediate hops)
- Sortable numeric columns (default Relayed desc, nulls always last)
- Token -> contact enrichment via /api/contacts/cached?format=full
  pubkey prefix match: single candidate shows the name, collisions show
  'ambiguous (n)' with candidate names in the tooltip (matcher shared
  with the stage-5 map)
- Clicking a stats row applies that token as the message filter and
  switches back to the Messages view

Verified live via Playwright: aggregates match an in-page manual
recount exactly, sort orders correct, row-click cross-navigation works,
1-byte and 2-byte tokens kept distinct.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:12:34 +02:00
MarekWo 07d2c347ba feat(pathanalyzer): path display and filters (stage 3)
- Message rows expand to show each echo as per-hop repeater hash chips
  (A3 -> 3B -> 5E) with direction badge, SNR and received time; tokens
  split per that echo's own hash_size (same logic as showPathsPopup),
  0-hop echoes shown as 'Direct (flood, 0 hops)'
- Chip click copies the repeater hash, echo line click copies the comma
  route (parity with the chat path popup)
- Filter bar (client-side, combinable, 150ms debounce): hop count
  (any/0/1/2/3/4+, matches if any echo has that hop count), repeater
  hash token (hex-normalized prefix match, bridges mixed hash sizes),
  sender substring; Clear button appears when any filter is active
- 'N of M messages' counter and a dedicated empty state when filters
  match nothing

Verified live via Playwright: expand/collapse, both copy actions,
hops=2 yields only rows with a 2-hop echo (39/602), combined
token+hops filter, sender filter, clear resets to full set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:01:23 +02:00
MarekWo 9668651a61 feat(pathanalyzer): Path Analyzer panel skeleton (stage 2)
Full-screen iframe panel wired per the add-fullscreen-panel checklist:
- /path-analyzer Flask view + standalone template (theme bootstrap before
  CSS, local vendor assets, Leaflet included for the stage-5 map view)
- path-analyzer.js: days selector (1/3/5/7), message table (time, channel,
  sender, preview, copyable packet hash, hops, echo count), spinner/empty
  states, local toast helpers fed from /api/ui/settings
- #pathAnalyzerModal in index.html, iframe src set on show.bs.modal for
  fresh data each open; FAB button + purple gradient class
- menu entry in #mainMenu (Tools) + placement radio row in Settings ->
  Appearance; registered in ITEM_PLACEMENT_DEFS/DEFAULTS (default: menu)
- style.css: #pathAnalyzerModal added to all three narrow-viewport
  fullscreen-override selector lists

Verified live via Playwright at 1280px and 390px, light + dark: modal
fills the viewport exactly, placement radios move the entry both ways,
iframe reloads on every open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 17:14:20 +02:00
MarekWo 92f5a7edca feat(repeaters): Settings tool (stage 7)
- GET/POST /api/repeaters/<pk>/settings?section=X: sequential text-CLI
  get/set batches via repeater_cmd_wait; per-field values/errors on read,
  per-field ok|failed|reboot_required classification on write (reply
  starting ok/password now = ok, mention of reboot = reboot_required);
  admin-gated via new _require_repeater_admin helper (also reused by /cli)
- Settings pane: accordion with 8 sections (Basic, Radio, Location,
  Features, Network health, Advertisement, Operator info, Advanced),
  lazy-loaded on first expand (each field is one mesh round-trip),
  per-section Refresh/Apply, dirty tracking with header badges
- Field types: on/off switches, 0/1 switches, select (loop.detect),
  composite radio (freq,bw,sf,cr) with reboot warning + confirm dialog,
  write-only admin password (updates the saved password on success),
  owner.info textarea with pipe-to-newline mapping
- Failed writes keep the field dirty and surface the firmware reply
  (e.g. "Error: interval range is 60-240 minutes") under the input
- Strip "%" suffix when loading number fields (get dutycycle -> "70.0%")

Verified live against PL-KRA Wegrzce 2: all 8 sections read cleanly,
flood.max 64->63->64 write round-trip, firmware range rejection surfaced
per-field, radio confirm-cancel path, dutycycle % parsing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 14:43:16 +02:00
MarekWo 4fb6b2a847 fix(repeaters): CLI terminal fills the viewport vertically
height: max(260px, calc(100vh - 430px)) instead of a fixed 340px, so
the scrollback grows with the window while staying usable on small
screens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:57:26 +02:00
MarekWo 3e80ea0c85 feat(repeaters): CLI tool (stage 6)
Remote text console for the managed repeater. Core mechanism:
repeater_cmd_wait() sends the command and synchronously waits for the
reply — CLI replies arrive as CONTACT_MSG_RECV txt_type=1 with no
protocol-level correlation, so correlation = repeater lock (single
command in flight) + sender-prefix match. A single-slot waiter is
checked at the top of _on_dm_received: matched CLI replies are
consumed there and never stored as chat DMs; unmatched ones (e.g.
console fire-and-forget cmd) keep the legacy DM behavior. Wait time
derives from the device-suggested timeout (10-45 s clamp).

POST /api/repeaters/<pk>/cli is admin-gated (403 for guest sessions:
firmware silently drops guest text commands, which would look like a
timeout). Pane: dark terminal styled after the Console module, quick-
command chips, Enter to send, per-repeater arrow-key history in
localStorage, elapsed-time line, inline timeout errors (lost replies
happen over radio — a manual retry typically succeeds).

Settings (stage 7) and Actions (stage 8) will reuse repeater_cmd_wait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:45:51 +02:00
MarekWo 2b6d82a11c feat(repeaters): Neighbors tool with map view (stage 5)
List view: all zero-hop neighbours (fetch_all_neighbours paginates the
firmware's ~14-entry pages), rows show resolved contact name (or
[pubkey prefix] for unknown repeaters), heard-ago and SNR; count in
the toolbar. Names/positions are enriched server-side by prefix-
matching device contacts with the DB contact cache as fallback.

Map view (List/Map toggle, shown only when something is mappable):
Leaflet with the managed repeater as a red marker, positioned
neighbours in green, dashed connection lines labeled with permanent
SNR tooltips, and a footnote counting neighbours without a known
position. Toggle hidden entirely when no coordinates exist.

Verified live on PL-KRA Wegrzce 2: 37/37 neighbours fetched, 20
mappable, SNR labels rendered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:23:43 +02:00
MarekWo f288586ea6 feat(repeaters): Repeater Management panel (stage 2)
/repeaters/manage?pubkey=... — per-repeater management panel opened
automatically after login from the My Repeaters list:
- header card: name, shortened pubkey with copy, current path,
  location, ADMIN/GUEST badge from the captured login session
- tools grid (Status / Telemetry / Neighbors / CLI / Settings /
  Actions) with pane placeholders; CLI+Settings+Actions are locked
  for guest logins (firmware accepts text CLI from admins only)
- auto-login with the saved password when the in-memory session is
  gone (e.g. after app restart), password-modal fallback with retry
- REST: GET /api/repeaters/<pk> (merged entry + session state),
  GET .../session, POST .../logout

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 07:24:52 +02:00
MarekWo ca2a6bacf9 feat(repeaters): My Repeaters panel (stage 1)
New full-screen panel (main menu / FAB) for repeater administration:
- repeaters table (saved per-pubkey admin password, login metadata);
  plaintext per observer_brokers precedent (single-user LAN app)
- REST /api/repeaters: list merged with device contact truth, add
  (device REP contacts only), set/clear password, remove, login
- device_manager: _repeater_lock serializes all repeater ops (companion
  firmware has a single pending-request slot); repeater_login now
  filters LOGIN_SUCCESS by pubkey_prefix and captures is_admin/
  permissions into an in-memory session store
- login timeout UX: wrong password and unreachable repeater are
  indistinguishable (firmware stays silent) - error message names both
- panel UI: add-picker, password modal (remembered password), per-
  repeater path editor reusing /api/contacts/<pk>/paths + Leaflet
  repeater map picker (adapted from the DM panel)
- meshcore pin bumped to >=2.3.7 (send_login_sync era API, fixed
  LOGIN_SUCCESS/LOGIN_FAILED parsing; container already ships 2.3.7)

Stage 2 (management panel with Status/Telemetry/Neighbors/CLI/
Settings/Actions tools) follows after user review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 22:55:44 +02:00
MarekWo 668d8d5c79 feat(observer): Settings > Observer tab UI
New Observer tab: master switch, IATA location code, flood advert
interval, live status line with packet counters, broker list with
connection badges (green/red + error tooltip), enable switches and a
stacked add/edit modal (blank password on edit keeps the stored one).
Live badge/counter updates via the observer_status socket event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 19:07:09 +02:00
MarekWo debb711b71 feat(analyzer): seed Letsmesh as a normal DB row and flip switch UX
Letsmesh Analyzer is now a regular analyzers row, seeded once via an
app_settings flag on first startup. It can be renamed, disabled, or
deleted like any user entry. The frontend drops the special built-in
pseudo-row and the built-in slot in the chooser.

Click resolution updated: 0 enabled → toast "No analyzer configured";
1 enabled → open directly; multiple with a default → open default;
multiple without default → chooser.

The row switch and the edit modal now show "Enabled" (checked = active),
which matches intuition. The DB column remains is_disabled so no data
migration is needed — the UI simply inverts the value on read/write.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-01 21:56:55 +02:00
MarekWo f72f6d418a feat(db): VACUUM after retention and an Optimize button in Backup modal
SQLite DELETE marks pages free but doesn't shrink the file, so the
new retention job would keep DBs at their bloated size forever without
a follow-up VACUUM. Add db.vacuum() that runs PRAGMA-free VACUUM and
reports size_before/size_after/elapsed so callers can surface results.

The retention job now calls vacuum() automatically when it deleted at
least 1000 rows. Threshold avoids the multi-second VACUUM cost on quiet
days. Failure is logged, not raised — a missed VACUUM never crashes
the scheduler.

Power-user override: new "Optimize now" button in the Database Backup
modal triggers VACUUM on demand via POST /api/db/vacuum, alongside a
GET /api/db/size that drives the live "Current size" label. This way
users don't have to wait until 03:30 to reclaim space after the first
big retention pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 10:55:23 +02:00
MarekWo 10792b8566 feat(analyzer): add configurable analyzer services in Settings
Add a Settings > Analyzer tab letting users CRUD custom MeshCore Analyzer
services with a star-toggle default and inline disabled switch. The chart
icon under each group-chat message now resolves at click time: built-in
Letsmesh when no enabled customs, the default when set, or a chooser
modal otherwise. Backend stops shipping the prebuilt analyzer_url and
emits packet_hash instead — the frontend substitutes {packetHash} in the
chosen URL template.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 15:34:45 +02:00
MarekWo 8da9ee2455 fix(ui): sidebar breakpoint not applied in DM window
dm.html is a standalone template (not extending base.html), so it didn't
receive the .layout-wide class — the DM contact list never appeared as
a sidebar even on wide screens.

Added an inline script in dm.html <head> mirroring the base.html logic.
Also subscribes to the `storage` event so the iframe re-applies the
class live when the user changes the breakpoint in the parent window's
Settings -> Interface tab.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 21:48:51 +02:00
MarekWo 927fc518f0 feat(ui): user-configurable sidebar breakpoint width
The threshold above which the channel/DM list shows as a sidebar (vs.
collapsing to a top dropdown) is now user-configurable in
Settings -> Interface -> Layout. Persisted per device in LocalStorage
(key: mc-webui-sidebar-breakpoint, default: 992px, range: 600-2000).

Implementation: replaced hardcoded `@media (min-width: 992px)` with a
`.layout-wide` class on <html>, toggled by JS based on window.innerWidth
vs. the user's breakpoint. An inline script in <head> applies the class
synchronously to prevent layout flash on page load (same pattern as theme).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 21:35:56 +02:00
MarekWo a571d5388d fix(console): open at bottom, drop Disconnected from transcript, add jump-to-latest button
- Stop persisting "Disconnected" / "Failed to connect" — these are session-local events; saving them made every reopen begin with a stale red error.
- Scroll to the bottom after restoring transcript so reopens land at the latest entry instead of the top.
- Add a floating chat-style jump-to-latest button that appears whenever the user scrolls more than ~80px above the bottom and disappears once they're back at the latest entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 07:42:51 +02:00
MarekWo 3ef1eac0be feat(console): rename to mc-webui, fix change_path, persist transcript
- Rename "meshcli Console" to "mc-webui Console" (modal title + docs).
- Drop redundant "Connected to..." messages; replace intro with a one-line "Type 'help' for available commands." hint.
- Use a teal device-name style so the header label is readable on the dark background.
- Display contact paths with commas (D1,90,05,54) instead of arrows in `contacts` and `path`, matching the standard MeshCore client.
- Fix `change_path`: previously read only args[2] after shlex split, silently writing a 1-byte path. Now joins remaining args, accepts comma/space/continuous-hex, validates hex, auto-deduces hash_size from comma-chunk length (1/2/3-byte hops), and routes through _change_path_async so path_hash_mode is set and the contacts cache is invalidated.
- Update `help` line and add a usage hint for the no-args form.
- Add capped persistent output transcript: GET/POST/DELETE /api/console/output (cap 500 entries). Console restores prior entries (faded) above a divider on open and exposes a trash button to clear it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 22:18:46 +02:00
MarekWo 58300f4a47 feat(ui): user-configurable Quick Access vs Main Menu placement
Lets users choose where each action appears: in the floating Quick
Access (FAB) bar or the slide-out Main Menu. Adds a "Hide Quick Access"
master switch and a per-item placement table in Settings -> Appearance.
Removes the stale "Refresh Messages" menu item (legacy of polling era,
WebSocket already covers it) and moves the Notifications toggle from
the menu to a dedicated Settings -> Notifications tab.

Each of the 11 configurable items is rendered in both locations;
applyItemPlacements() toggles d-none based on localStorage. Badges for
DM unread and pending contacts propagate to the Main Menu copies so
they stay in sync regardless of placement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 21:37:55 +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 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 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 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 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 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 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 710f69c350 feat: add BLE transport support for companion devices
Integrate meshcore library's BLE connection (via bleak) as a third
transport option alongside serial and TCP. Priority: BLE > TCP > Serial.

Config: MC_BLE_ADDRESS and MC_BLE_PIN environment variables.
Docker: bluez/dbus packages, NET_ADMIN cap, D-Bus socket mount.
UI: transport type badge in navbar, transport_type in /api/status.
Watchdog: skip USB reset for BLE connections (same as TCP).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 10:03:45 +02:00
MarekWo d2e019fa0e refactor(dm): restructure retry logic into 4-scenario matrix
Replace 3-way branching (configured_paths/has_path/else) with
4-scenario matrix based on (has_path × has_configured_paths):

- S1: No path, no configured paths → FLOOD only
- S2: Has path, no configured paths → DIRECT + optional FLOOD
- S3: No path, has configured paths → FLOOD first, then ŚD rotation
- S4: Has path, has configured paths → DIRECT on ŚK, ŚD rotation, optional FLOOD

Key changes:
- S3: FLOOD before configured paths (discover new routes)
- S4: exhaust retries on current ŚK before rotating ŚD
- S4: dedup ŚG/ŚK to skip redundant retries on same path
- Add _paths_match() helper for path deduplication
- Update tooltip text for settings clarity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 11:55:17 +01:00
MarekWo 292d1d91af fix(contacts): restore flexbox list height, remove calc() overrides
The contact list in Existing/Pending Contacts was not using all available
space due to calc(100vh - ...) and max-height rules overriding the
flexbox layout. Remove fixed height constraints from #pendingList and
#existingList in both contacts_base.html and style.css, letting the
flexbox chain (body > main > container > pageContent > list) fill the
remaining viewport space.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-27 07:54:47 +01:00