getById() returns null for deduped observations (not stored in byId).
Client filters on m.data.packet being truthy, so all deduped packets
were silently dropped from WS. Fallback to transmission or raw pktData.
- Add time window dropdown (15min default, up to 24h or All)
- Use 'since' param instead of fixed limit=10000
- Persist selection to localStorage
- Keep limit=50000 as safety net
Migration runs automatically on next startup — drops paths first (FK to
packets), then packets. Removes insertPacket(), insertPath(), all
prepared statements and references to both tables. Server-side type/
observer filtering also removed (client does it in-memory).
Saves ~2M rows (paths) + full packets table worth of disk.
- Server: support comma-separated type filter values (OR logic)
- Server: add observer_id filtering to /api/packets endpoint
- Client: fix type and observer filters to use OR logic for multi-select
- Client: persist observer and type filter selections to localStorage
- Keys: meshcore-observer-filter, meshcore-type-filter
CSS content:attr() doesn't support newlines in any browser. Replaced
with a real <span> child element with white-space:pre-line, shown on
hover via .sort-help:hover .sort-help-tip { display: block }.
Native title tooltips are unreliable (delayed, sometimes not shown).
Replaced with CSS ::after pseudo-element tooltip using data-tip attr.
Shows immediately on hover with proper formatting.
HTML entities like don't work inside JS template literals
(inserted as literal text). Setting .title via JS with actual \n
newlines works correctly in browser tooltips.
When Group by Hash is off, fetches all observations for multi-obs
packets and flattens them into individual rows showing each observer's
view. Previously just showed grouped transmissions without expand arrows.
- Region filter container: remove margin-bottom, use inline-flex align
- Column dropdown checkboxes: 14x14px to match region dropdown
- Sort help ⓘ: use for newlines in title (\n doesn't render)
- Dark mode: .filter-bar .btn.active now retains accent background
(dark theme override was clobbering the active state)
- All filter-bar controls now exactly 34px tall with line-height:1 and border-radius:6px
- col-toggle-btn matched to same height/font-size as other controls
- Controls grouped into 4 logical sections (Filters, Display, Sort, Columns) with vertical separators
- Added title attributes with helpful descriptions to all controls
- Added sort help icon (ⓘ) with detailed tooltip explaining each sort mode
- Mobile responsive: separators hidden on small screens
All filter bar controls now share: height 34px, font-size 13px,
border-radius 6px, same padding. Region dropdown trigger matches
other controls, menu widened to 220px with white-space:nowrap to
prevent text wrapping.
When switching to a non-Observer sort, batch-fetches observations for
all visible multi-observation groups that haven't been expanded yet.
Header rows update immediately without needing manual expand.
Sort was only applied in pktToggleGroup and dropdown change handler.
Missing from: loadPackets restore (re-fetches children for expanded
groups) and WS update path (unshifts new observations). Now all
three paths call sortGroupChildren after modifying _children.
- Removed per-group sort bar links (broken navigation)
- Added global 'Sort:' dropdown in filter toolbar
- Persists to localStorage across sessions
- Re-sorts all expanded groups on change
Two sort modes for expanded packet groups:
- Observer: group by observer, earliest first, ascending time within
- Path length: shortest paths first, alphabetical observer within
Sort bar appears above expanded children with bold active mode.
- Deeplinks now use ?obs=<observation_id> which is unique per row,
fixing cases where same observer has multiple paths
- Added '🔍 Trace' link in detail pane actions
During _loadNormalized(), observations load in DESC order so the first
observation processed is the LATEST. tx.observer_id was set from this
latest observation. Added post-load pass that finds the earliest
observation by timestamp and sets tx.observer_id/path_json to match.
The WS handler was overwriting the group's path_json with the longest
path from any new observation. Header should always show the first
observer's path — individual observation paths are in the expanded rows.
Previously db.seed() ran unconditionally on startup and would populate
a fresh database with fake test data. Now seeding only triggers when
explicitly requested via --seed CLI flag or SEED_DB=true env var.
The seed functionality remains available for developers:
node server.js --seed
SEED_DB=true node server.js
node db.js (direct run still seeds)
When viewing a specific observation, the URL now includes ?obs=OBSERVER_ID.
Opening such a link auto-expands the group and selects the observation.
Copy Link button includes the obs parameter when an observation is selected.
Removed server-side longest-path override in /api/packets/:id that
replaced the transmission's path_json with the longest observation
path. The header should always reflect the first observer's path.
Individual observation paths are available in the observations array.
When expanding a grouped packet and clicking a child observation row,
the detail pane now shows that observation's observer, SNR/RSSI, path,
and timestamp instead of the parent packet's data.
Child rows use a new 'select-observation' action that builds a synthetic
packet object by overlaying observation-specific fields onto the parent
packet data (no extra API fetch needed).
When a later observation has an earlier timestamp, the transmission's
first_seen was updated but observer_id and path_json were not. This
caused the header row to show the wrong observer and path — whichever
MQTT message arrived first, not whichever observation was actually
earliest.