Commit Graph

280 Commits

Author SHA1 Message Date
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 a036d6af6c feat(repeaters): Actions tool (stage 8)
- POST /api/repeaters/<pk>/action {action}: whitelisted one-shot commands
  via repeater_cmd_wait — zerohop_advert (advert.zerohop), flood_advert
  (advert), clock_sync (clock sync), reboot; admin-gated
- Replies surfaced verbatim with an ok flag (reply starts with OK), so
  firmware refusals like "ERR: clock cannot go backwards" show as-is
- reboot special case: the firmware restarts without ever replying, so a
  clean send followed by silence reports success ("Reboot command sent")
- Actions pane: action rows with inline result lines, flood advert
  styled as warning ("Not recommended - high network load"), Danger zone
  card with confirm()-guarded Reboot and a muted note that erase is only
  available on the USB serial console (firmware restriction)

Verified live against PL-KRA Wegrzce 2: zero-hop advert replied
"OK - zerohop advert sent" and the advert arrived back at our device
(log: Advert from 'PL-KRA Wegrzce 2' type=2); clock sync surfaced the
firmware refusal; reboot confirm-cancel path checked in Playwright
(reboot itself intentionally not executed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 19:22:48 +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 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 995cb49f4c feat(repeaters): Telemetry tool (stage 4)
All Cayenne LPP channels shown at once (no channel dropdown like the
standard app): one card per channel with typed rows — icon, name,
value and unit (voltage V, temperature °C, current A, power W,
humidity %, GPS lat/lon+alt, etc.). Channel 1 is labeled 'device'
(repeater's own vitals). Refresh + updated-ago label; timeout renders
inline with a retry button (exercised for real on a 2-hop repeater —
first attempt timed out, retry succeeded with 2 channels).

Backend: repeater_req_telemetry in device_manager (serialized under
the repeater lock; the old name-only request_telemetry stays for
sensor nodes) + login-gated GET /api/repeaters/<pk>/telemetry.
GPS values arrive as {latitude, longitude, altitude} objects from
meshcore 2.3.7 — formatter handles both object and array forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:36:02 +02:00
MarekWo 707fd20426 feat(repeaters): Status tool (stage 3)
First management tool. The Status tile auto-fetches on open and shows
a compact three-section table (not MO's oversized tiles):
- System: battery %/V (linear 3.3-4.2 V estimate), uptime, repeater
  clock, queue length, debug/error events
- Radio: last RSSI/SNR, noise floor, TX/RX airtime
- Packets: sent + received (flood/direct split), duplicates, RX
  errors, channel utilization computed client-side as
  (tx_air+rx_air)/uptime (matches the firmware's own 10.09% reading)
Refresh button + "updated Ns ago"; errors render inline with retry.

Backend: GET /api/repeaters/<pk>/status and /clock, both gated on an
existing login session (401 need_login) to fail fast instead of a
2-min timeout. Clock loads as a follow-up request so the table
appears immediately, then the clock row fills in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:16:58 +02:00
MarekWo d825f4dc29 fix(repeaters): remove 8px modal margin on narrow viewports
The fullscreen-modal override (@media max-width:576px) that zeroes the
default .modal-dialog 0.5rem margin enumerates modal IDs by hand and
omitted #repeatersModal, so on narrow windows the My Repeaters modal
kept an 8px top/left gap that Contact Management did not. Added
#repeatersModal to the three override selector lists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 08:12:04 +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 ca4c77da36 feat(chat): require explicit send button on touch devices
Enter on a phone's virtual keyboard now inserts a newline instead of
sending, in both group chat and DM. Detected via the pointer:coarse
media query - no user-facing setting, since accidental sends from a
mistapped Enter were the actual complaint. Desktop keeps Enter-to-send.
2026-06-29 09:31:50 +02:00
MarekWo 7be3e26161 fix(dm): preserve line breaks in sender's own DM message bubbles
.dm-content was missing white-space: pre-wrap, unlike .message-content
in the group chat, so multi-line DMs collapsed to one line on the
sender's own screen even though the recipient saw them correctly.
2026-06-29 09:31:35 +02:00
MarekWo 0eb7c772a7 fix(status): stop the connection badge from lying about device state
loadMessages() called updateStatus('connected') on every successful
/api/messages fetch -- a DB read that succeeds regardless of whether
the meshcore device itself is connected. Same conflation in dm.js,
which marked 'connected' on Socket.IO transport connect (browser <->
server), unrelated to device <-> server connectivity. Together these
silently overwrote the real status (e.g. an actual device disconnect)
back to "Connected" on the very next channel switch or auto-refresh.

Also fixed the 'device_status' socket handler in app.js, which built
its own markup against a `#connectionStatus` element that doesn't
exist in any template (dead code since introduction) instead of
calling the shared updateStatus() against the real `#statusText`
element. Added a 60s polling fallback via loadStatus() in case a
push is ever missed on a long-lived tab.

Verified locally: ran the app against no real device (always
disconnected) and confirmed via Playwright that the badge now stays
correctly "Disconnected" through repeated loadMessages() calls,
instead of flipping back to "Connected" as before the fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 19:04:41 +02:00
MarekWo 2f8f765af5 fix(channels): backfill raw-resend buttons when displayMessages races loadStatus
loadStatus and loadMessages fire in parallel at page init. Whichever lost
the race left own-message bubbles without the raw-resend button — and once
the row was rendered, neither displayMessages (no re-render on switch
without going back through createMessageElement, which is gated on
window.deviceCaps at the time of call) nor refreshMessagesMeta (skips rows
that already have route info) would patch it back in. So the button only
ever appeared for messages sent in the current session.

Walk visible own bubbles in two places — at the end of loadStatus and at
the end of displayMessages — and inject the button on any row missing it.
Idempotent (skips bubbles that already have .btn-raw-resend), cheap (no
network), and covers both the page-reload race and the channel-switch /
archive-view paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 19:01:07 +02:00
MarekWo e3211dd536 fix(channels): don't render raw-resend button against a _pending_<ts> id
The optimistic-send path renders the bubble with msg.id = "_pending_<ts>"
before the API confirms. The previous PR baked that string straight into
onclick="resendChannelMessageRaw(_pending_<ts>, this)" — an undefined
identifier — so the first click after sending threw ReferenceError and
nothing happened.

Skip the raw-resend button entirely while msg.id is non-numeric, then run
a one-shot refreshMessagesMeta([real_id]) right after the optimistic id
swap so the button shows up immediately even on channels where no echoes
arrive (so the existing echo-driven inject path never fires).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 18:07:04 +02:00
MarekWo 00f0544a47 feat(channels): UI for raw resend + clarify the edit-message button
PR #5 of 5. Wires the user-facing controls for the raw-resend feature.

Channel messages (own):
- The existing arrow-repeat button only pasted content into the composer
  for hand-edits, which the "Resend" tooltip mis-named as a true resend.
  Rename it to "Edit message" with a pencil-square icon.
- Add a new arrow-repeat button that POSTs to /api/messages/<id>/resend.
  Tooltip explains the actual semantics ("rebroadcast same packet so
  unreached repeaters can pick it up"). Spins .btn icon while in flight,
  shows a toast on result. Rendered only when the cached
  /api/status.supports_raw_resend is true (firmware ≥1.16).
- Inject the same buttons in updateMessageMetaDOM so history items loaded
  before window.deviceCaps was populated still get the new button on the
  next echo-driven meta refresh.

DM messages: rename the equivalent paste-button to "Edit message" with
the same pencil icon for UI consistency. The protocol-level retry stays
unchanged — there's no per-DM raw resend button (auto-retry covers it).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 15:02:25 +02:00
MarekWo d23e865f35 feat(channels): merge post-resend echoes into existing repeater badge
PR #4 of 5. After a successful resend, re-arm _pending_echo with the
original msg_id and known pkt_payload so echoes from previously-unreached
repeaters that pick up the rebroadcast are classified as 'sent' and carry
msg_id in the SocketIO emit.

The frontend echo handler now collects forced msg_ids and passes them to
refreshMessagesMeta(forceIds), which bypasses the "already has route info,
skip" guard for those ids. End result: clicking resend extends the
repeater list on the existing message's badge in place — no duplicate row,
no stale count.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 14:32:44 +02:00
MarekWo f1477d84ac fix(db): run VACUUM in a worker thread to survive proxy timeouts
The reverse proxy fronting mc.wojtaszek.it closes idle HTTP responses
after ~30 s, so the manual Optimize endpoint timed out client-side
even though SQLite finished VACUUM and the Flask handler logged a 200.
The user saw "Optimize failed" while the DB had actually shrunk.

Split the endpoint into kickoff + polling: POST /api/db/vacuum spawns
a daemon worker thread, stores state in a module-level dict guarded by
a lock, and returns 202 immediately. GET /api/db/vacuum/status returns
{running, elapsed_seconds, ...} so the UI can poll every 2 s and show
the same "freed X bytes in Y s" toast once the worker is done. A
second POST while a VACUUM is in flight returns 409 instead of starting
a parallel rewrite.

Client polls for up to 10 minutes (300 × 2 s) before surrendering with
a "still running" warning — well past any real VACUUM duration we'd
expect, but bounded so a server-side crash can't leave the UI
spinning forever.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 12:30:09 +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 1d47c9c0e8 fix(perf): polling-only Socket.IO + channels DB fallback on USB timeout
Werkzeug dev server can't upgrade WebSockets, so every io() upgrade attempt
returned HTTP 500 and clients fell into a polling/upgrade reconnect loop —
visible as 10-15s freezes on app load. Force transports: ['polling'] on
/chat, /console and /logs clients; long-poll keeps real-time pushes
working with ~1-2s latency.

When the MeshCore device briefly stalls, get_channel_info() used to block
on the default 30s timeout per slot, so iterating max_channels slots could
take minutes; in practice only Public answered and the rest timed out,
leaving the UI with just one channel. Drop per-call timeout to 3s, raise
TimeoutError to the caller, and have cli.get_channels() break on first
timeout and merge the remaining slots from the channels table in the DB
(which already mirrors device state via upsert_channel).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 07:31:47 +02:00
MarekWo f34c95c15b fix(analyzer): dim Settings backdrop behind nested analyzer modals
Bootstrap stacks backdrops at z-index 1050, which falls below the open
Settings modal at 1055. Without bumping it, the Add analyzer and chooser
modals appeared without a visible backdrop. Mirror the coordPickerModal
fix by raising the latest backdrop to 1075 on shown.bs.modal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 21:35:23 +02:00
MarekWo f0cb354ae0 fix(analyzer): wrap long URLs in Settings list on narrow mobile viewports
Without min-width:0 on the flex-grow column and explicit word-break on
the <code>, a long URL with no spaces would refuse to wrap on real
Samsung S20 / Chrome Mobile, pushing the switch and edit/delete buttons
off-screen. DevTools mobile emulation hid the bug because its <code>
wrapping defaults differ slightly from the real device.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 21:20:54 +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 e06e7b06dd feat(dm): apply a configured path to the device from Contact Info
Adds an upload-arrow button to every entry in the Paths list inside the
DM Contact Info modal: clicking it pushes that configured path to the
firmware as the active route, mirroring the console's change_path
command but without leaving the UI. After the device confirms, the
modal's device-path line refreshes so the new route is reflected
immediately.

Backend: POST /api/contacts/<pubkey>/paths/<id>/apply looks up the
configured path, runs dm.change_path() with its hex + hash_size, and
invalidates the contacts cache.
2026-06-05 13:43:19 +02:00
MarekWo bcaa550809 fix(dm): persist delivery_path_hash_size so reloaded bubbles render multi-byte routes
Live dm_delivered_info already carried the correct hash_size, but the
DB row only kept delivery_path. After a reload the API filled in
path_hash_size from the incoming path_len column (NULL for outgoing
DMs → default 1), so 2-byte routes were re-rendered as single-byte
hops.

Added a delivery_path_hash_size column (auto-migrated, defaults to 1)
that update_dm_delivery_info now stores alongside the delivery path,
populated from the same hash_size already known by each delivery path
(retry ctx, PATH event, delayed contact backfill). /api/dm/messages
returns the new field; dm.js prefers it over path_hash_size when
rendering the Route line, falling back to the old field for legacy
rows.
2026-06-05 09:22:51 +02:00
MarekWo 4effa47fe1 fix(ui): multi-byte path rendering across contact list, DM modal, retry
Same root cause as the previous console fix: meshcore lib 2.x stores
out_path_len as the masked hop count and out_path_hash_mode separately.
Several UI surfaces and the DM retry logic were still decoding the
hash-size mode from the upper bits of out_path_len, which always yields
1 for in-memory contact data and silently truncates multi-byte paths.

Fixed sites:
- /api/contacts/detailed: path_or_mode and outgoing payload now use
  out_path_hash_mode; the field is included in /api/contacts too.
- dm.js: Contact Info modal computes hashSize for the import button
  from out_path_hash_mode.
- console "contacts" command: same correction as "path".
- device_manager._paths_match / _extract_path_hex: accept hash mode as
  a parameter; callers (_dm_retry_task, _delayed_path_backfill, Phase 2
  rotation dedup) pass contact.out_path_hash_mode.
- PATH event handlers: derive hash_size from path_hash_mode instead of
  decoding it from an already-masked path_len.
2026-06-05 08:54:29 +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 8f144c6e97 fix(console): jump to latest when modal opens
The console iframe lives inside a Bootstrap modal that is hidden on page load. While the modal is hidden the messages container has 0 height, so the scrollToBottom() that runs after loadOutputHistory() is a no-op. When the modal opens the container resizes to its real height but stays scrolled to the top.

Watch the container with a ResizeObserver and scroll to the bottom whenever its height transitions from 0 to non-zero, so the transcript opens at the latest entry on first show and on every reopen.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-06 07:55:23 +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 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 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