A tester's cached-contacts map came up empty and Path Analyzer drew routes
across the globe. Two of his 2131 cached contacts carried garbage positions
from corrupted adverts - lat 1642 / lon -323 and lat 309 / lon -1768.
Leaflet clamps latitude when projecting but not longitude, so fitBounds()
spanned 1797 degrees, dropped to zoom 0 and centred on lon -870; all 2273
real markers were rendered several world-widths outside the viewport.
Reproduced against his DB: zoom 0, bounds -1431..-309. The device-only view
was fine because both bad rows are cache-only.
Four layers, so no single miss can blank a map again:
- app/geo.py: one definition of a usable coordinate, applied on write in
Database.upsert_contact() so the cache stops storing garbage, and on API
output so a cache poisoned by an older build heals itself with no
migration. Corrupted advert types (61, 126, 152, 210 seen in the wild)
are logged but kept raw - they already render as UNKNOWN.
- static/js/geo-utils.js: hasValidGps()/withValidGps()/fitMapToPoints()
replace eight copies of the same check across six files. Path Analyzer
had a looser variant that also accepted lon == 0, which put nodes off
west Africa and stretched routes across the map; it now shares the rule.
- Per-marker try/catch and a bounds validity check, so an entry that still
slips through costs its own pin rather than the whole map.
Verified against the tester's DB in a container: cached map 2273 markers,
lon span 1797 -> 19 degrees, centred on Poland; the route that drew the
world-spanning line (msg 29499, token 72AF) now renders as a real
Krakow-Kielce path with the bad hop left unresolved. Live local instance
regression-checked on all five map pages. tests/test_geo.py: 43 passed;
full suite 290 passed, 2 pre-existing failures unrelated to this change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iOS WebKit zooms the whole page when a focused form control computes to
under 16px. `#messageInput` was 0.9rem (14.4px) on narrow screens, so
every tap on the message box tripped it - reported on iPhone in both
Safari and Chrome, on a phone with no configuration of its own.
The zoom also never undid itself: the shell is `height: 100dvh;
overflow: hidden`, so WebKit has nothing to scroll back and leaves the
page scaled until the user pinches out.
Raise every focusable control to 16px on (pointer: coarse), which also
covers landscape phones wider than the 768px breakpoint. Desktop keeps
Bootstrap's smaller sizing. The guard uses !important on purpose: it has
to outrank page-local <style> blocks that load after style.css and reach
specificities like `.filter-sort-toolbar .form-select`.
Fields that only open a picker (checkbox, radio, range, color, file,
buttons) are excluded - they never trigger the zoom.
The contacts toolbar was the one layout that could not take the bump:
three 16px selects overflow one unwrapped flex row, clipping the sort
control, so it now wraps.
Verified locally at 390x844 and 932x430 with touch emulation: no control
under 16px on any route, no horizontal overflow, desktop unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 keys across contacts_base.html and the four contacts-* pages: the shared
delete-confirm modal and toast, the map modal, the three nav cards, the whole
cleanup form, the filter bars, and the add-contact URI/QR/manual forms.
Node types stay English throughout — the COM/REP/ROOM/SENS codes and their
"(Companion)" / "(Room Server)" expansions. The codes are also what the JS filters
compare against, so translating the labels would be a step toward breaking the
type filter.
contacts.js is the other half of this stage and follows separately; it is 2771
lines and carries roughly twice the string count of these five templates.
The hit-count guard earned its keep again: it refused to touch contacts-add.html
and contacts-pending.html because "Name:", "Key:", "Type:", "Cancel" and the
close aria-label each appear two or three times. All were genuinely the same
label reused, so they share a key — but the script stopped rather than guessing,
and the three files it had already processed were left correctly applied.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Groundwork for translating the interface. No strings are extracted yet — this
stage only adds the mechanism, so the UI is byte-identical in English.
Translations are one flat JSON file per language. An admin can add a language by
dropping <lang>.json into $MC_CONFIG_DIR/translations/ and picking it in
Settings > Appearance — no rebuild, no restart, no compile step. A drop-in file
overrides a built-in one of the same name. Built-ins: en (source) and pl.
How it works:
- app/i18n.py loads and merges catalogs over en.json, so per-key English
fallback is baked in server-side and the JS runtime needs no fallback logic.
Catalogs are fingerprinted with stat() per render, so a dropped-in file is
live on the next refresh rather than the next restart — which matters,
because restarting drops the device connection for up to 60s.
- Template text renders server-side via t()/t_html()/tn() from inject_globals(),
so the first paint is already correct. That context processor covers every
render_template() in the app, including the six standalone iframe pages, so
routes/views.py needed no changes.
- JS text comes from /i18n/<lang>.<hash8>.js, a blocking immutable script shared
by all 8 entry points. The hash is in the path, not a query string, so
intermediary caches and the service worker bust reliably.
- Language = per-browser mc_lang cookie over a server-wide DB default. The
cookie is what makes iframes work: they are same-origin, so they send it
automatically and the existing modal-open reload wiring needs no changes.
t() deliberately does not escape — Jinja autoescape handles that, and escaping
here would double-escape every French apostrophe. t_html()/tHtml() escape their
params but trust catalog markup.
Also fixed along the way:
- save_ui_settings() replaced the whole settings blob instead of merging, so a
language-only POST would have wiped the toast settings. It worked before only
because the one form always submitted every key.
- `const t = document.getElementById(...)` in populateUiSettingsForm shadowed
the global translation helper; same for `var t` in six theme IIFEs.
scripts/i18n_check.py now fails the build if that pattern comes back.
- Removed app/templates/contacts.html, dead since the contacts pages were split
(no render_template reference anywhere), and two stale CSS comments.
Verified locally: catalog route headers/security, drop-in without restart,
malformed catalogs logged and skipped rather than offered in the picker,
cookie propagation into iframes, and 360px layout. Python and JS agree on
interpolation, escaping and Polish plural categories.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
- Create theme.css with CSS custom properties for light/dark themes
- Dark theme inspired by demo landing page (deep navy palette)
- Update style.css: replace ~145 hardcoded colors with CSS variables
- Extract inline styles from index.html, contacts.html, dm.html to style.css
- Add Appearance tab in Settings modal with theme selector
- Bootstrap 5.3 data-bs-theme integration for native dark mode
- Theme persisted in localStorage, applied before CSS loads (no FOUC)
- Console and System Log panels unchanged (already dark themed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The MeshCore community uses "companion" not "client" for type 1 nodes.
Rename the CLI label to COM across all UI, API, JS, and docs to align
with official terminology. Includes cache migration for old CLI entries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace fixed calc() heights with flexbox layout so the contact list
fills all remaining viewport space on any screen size
- Make body/main/container chain flex columns so the list can grow
- Reduce vertical spacing between contact name, public key, and
last advert rows for more compact cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces two sort toggle buttons with a single <select> dropdown (e-commerce style)
so all 3 filter/sort controls fit on mobile screens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove separate "Copy Key" button
- Make public key prefix clickable to copy
- Add fallback for HTTP contexts (execCommand)
- Add hover and copied state styling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add ability to protect contacts from accidental deletion via Contact
Cleanup tool. Protected contacts are stored locally in settings file.
Changes:
- Add GET /api/contacts/protected endpoint
- Add POST /api/contacts/<key>/protect toggle endpoint
- Add is_protected field to /api/contacts/detailed response
- Exclude protected contacts from cleanup preview and deletion
- Add Protect toggle button to Existing Contacts list
- Show lock icon for protected contacts
- Disable Delete button for protected contacts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Convert type filter checkboxes to clickable toggle badges on the Map
modal (accessible from main menu). Same UX pattern as pending contacts:
- Active: colored background, white text
- Inactive: colored text/border, white background
Updated in both base.html and contacts_base.html for consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace type filter checkboxes with clickable toggle badges in Pending
Contacts (CLI/REP/ROOM/SENS) - more compact and better mobile UX
- Make Pending Contacts buttons (Approve, Map, Copy Key) smaller and
uniform, matching Existing Contacts button style
- Optimize Existing Contacts filter toolbar to fit in one row on mobile
(narrower dropdown, more compact sort buttons)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use CircleMarker with different colors per contact type:
- CLI (blue), REP (green), ROOM (purple), SENS (orange)
- Add type filter checkboxes in map modal header
- Dynamically update markers when filters change
- Hide filter panel for single contact view
- Reduce message action buttons to 32x32px
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add Leaflet CSS/JS and map modal to contacts_base.html
- Add showContactOnMap function to contacts.js (contacts page has separate template)
- Load contactsGeoCache before messages to ensure Map buttons appear on bubbles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
After testing the modal implementation on Android PWA, removed duplicate
navigation elements and optimized layout to maximize screen real estate.
Changes to DM modal:
- Removed navbar with back button and "DM" title (duplicates modal header)
- Moved conversation selector to top of content as clean dropdown bar
- Changed main container from dynamic height to fixed 100vh
Changes to Contact Management modals:
- Removed navbar with home button from contacts_base.html
- Removed navbar_content blocks from all contact pages (manage, pending, existing)
- Removed padding/margins from base container (px-3 py-4 → p-0)
- Removed row gutters (added g-0 class)
- Removed col width constraint (col-lg-8 mx-auto → col-12)
- Added padding back to individual page content (p-3) for readability
Changes to modal close buttons:
- Replaced small X icon (btn-close) with proper button
- Added "Close" text with X icon for better visibility on mobile
- Used btn-outline-light styling for consistency with green/blue headers
Result:
- Modals now fill entire available screen space without margins
- No duplicate headers or navigation elements
- Clearer, more intuitive close action with labeled button
- Better mobile experience with larger touch targets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Root cause: Bootstrap offcanvas leaves backdrop/body classes in DOM
when navigating via window.location.href, causing viewport issues.
Changes:
- Remove hamburger menu button from DM navbar (caused overflow)
- Reduce menu button icon size on channels (remove font-size override)
- Add global navigateTo() function in app.js and contacts.js
- Function closes offcanvas, removes backdrops, clears body classes
- Replace all window.location.href calls with navigateTo()
- Updated navigation in: base.html, contacts*.html templates
- Add 100ms delay before navigation to ensure cleanup completes
This fixes the issue where:
1. Opening offcanvas menu on main page
2. Navigating to DM or Contact Management
3. Returning to main page
Results in corrupted viewport with hidden status bar
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Problem: #pendingList had max-height: 200px showing only 1-2 contacts
with huge unused whitespace below.
Solution:
- Desktop: height calc(100vh - 280px) - slightly more than existingList
due to extra description text
- Mobile: height calc(100vh - 320px) - adjusted for mobile layout
- Changed from max-height to height for consistent behavior
- Added min-height: 300px for safety
Now pending contacts list properly utilizes available screen space.
- Move toast container from bottom-right to top-left corner
- Reduce notification display time from 3-5s to 1.5s
- Prevents notifications from blocking message input area
- Applied consistently across all pages (main, DM, contacts)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Problem: List was too tall causing the entire page to scroll,
hiding navigation buttons at the top when scrolling to bottom.
Solution:
- Changed from max-height to height for #existingList
- Desktop: calc(100vh - 260px) - slightly smaller to fit all elements
- Mobile: calc(100vh - 300px) - adjusted for mobile layout
Now only the contact list scrolls internally, while navigation
buttons remain visible at the top.
Problem: #existingList ID selector had higher specificity than
.contacts-list-fullscreen class, and max-height values were limiting
the list height regardless of the class height setting.
Changes:
- Desktop: #existingList max-height calc(100vh - 400px) → calc(100vh - 240px)
- Mobile: #existingList max-height calc(100vh - 450px) → calc(100vh - 280px)
This should now properly utilize available screen space.
Changed height calculation:
- Desktop: calc(100vh - 300px) → calc(100vh - 240px)
- Mobile: calc(100vh - 340px) → calc(100vh - 280px)
This should eliminate the ~20% unused whitespace at the bottom
of the contact lists.
Adjusted .contacts-list-fullscreen height to better utilize available
screen space by increasing the subtracted value from 200px to 300px
on desktop and from 150px to 340px on mobile.
This accounts for:
- Navbar (~56px)
- Main container padding (~48px)
- Back buttons (~60px)
- Search toolbar (~50px)
- Filter/sort toolbar (~60px)
- Margins between elements (~26px)
Fixes blank space at bottom of Existing Contacts and Pending Contacts pages.
- Add CSS override to enable scrolling on Contact Management pages
(override global overflow: hidden from style.css)
- Move cleanup description to tooltip on info icon for consistency
with Manual approval toggle pattern
- Add flexbox layout to cleanup section h6 for proper icon alignment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>