- Track lastActivityMs (Date.now()) on each channel object instead of ISO lastActivity
- 1s interval iterates channels[] array and updates DOM text only (no re-render)
- Uses data-channel-hash attribute to find time elements after DOM rebuilds
- Simple formatSecondsAgo: <60s→Xs, <3600s→Xm, <86400s→Xh, else Xd
- Seed lastActivityMs from API ISO string on initial load
- WS handler sets lastActivityMs = Date.now() on receipt
- Bump channels.js cache buster
packet.timestamp is first_seen — when the transmission was originally
observed. When multiple observers re-see the same old packet, the
broadcast carries the original (stale) first_seen. For channel list
display, what matters is 'activity happened now', not 'packet was
first seen 10h ago'.
WS messages from lincomatic bridge lack packet.timestamp, so the
code fell through to payload.sender_timestamp which reflects the
MeshCore device's clock (often wrong). Use current time as fallback.
timeAgo labels were computed once on render and never updated,
showing stale '11h ago' until next WS message triggered re-render.
Added 30s interval to re-render channel list, cleaned up on destroy.
Multiple observers seeing the same packet triggered separate message
entries. Now deduplicates by packet hash — additional observations
increment repeats count and add to observers list. Channel messageCount
also only bumps once per unique packet.
Instead of re-fetching /api/channels and /api/channels/:hash/messages
on every WebSocket event, the channels page now processes WS messages
client-side:
- Extract sender, text, channel, timestamp from WS payload
- Append new messages directly to local messages[] array
- Update channel list entries (lastActivity, lastSender, messageCount)
- Create new channel entries for previously unseen channels
- Deduplicate repeated observations of the same message
API calls now only happen on:
- Initial page load (loadChannels)
- Channel selection (selectChannel)
- Region filter change
This eliminates all polling and WS-triggered re-fetches.
The channels WS handler was calling invalidateApiCache() before
loadChannels()/refreshMessages(), which nuked the cache and forced
network fetches. Combined with the global WS onmessage handler also
invalidating /channels every 5s, this created excessive API traffic
when sitting idle on the channels page.
Changes:
- channels.js: Remove invalidateApiCache calls from WS handler, use
bust:true parameter instead to bypass cache only when WS triggers
- channels.js: Add bust parameter to loadChannels() and refreshMessages()
- app.js: Remove /channels from global WS cache invalidation (channels
page manages its own cache busting via its dedicated WS handler)
The WS handler's 250ms debounce fired loadChannels() before the
global 5s cache invalidation timer cleared the stale entry, so
the fetch returned cached data. Now channels.js invalidates its
own cache entries immediately before re-fetching.
The /api/channels endpoint was returning simple numeric hash (e.g. '45') while
/api/channels/:hash/messages was using composite keys (e.g. 'ch_#LongFast',
'unk_45') internally. This mismatch meant no channel ever matched, so all
messages appeared encrypted.
Fix: return the composite key as the hash field from /api/channels so the
frontend passes the correct identifier. Also add encodeURIComponent() to
channel API calls in the frontend since composite keys can contain '#'.
After dedup migration, transmission IDs != old packet IDs.
Hash-based links (#/packets/HASH) are stable across the migration.
Affected: node detail, channel messages, live page packet cards.
- selectChannel updates URL to #/channels/<hash>
- init accepts routeParam and auto-selects channel
- Search results use new URL format instead of ?ch= query param
All cache TTLs now read from config.json cacheTTL section (seconds).
Client fetches config on load via GET /api/config/cache.
config.example.json updated with defaults.
Edit config.json, restart server — no code changes needed to tweak TTLs.
Two bugs:
1. refreshMessages() compared array length to detect changes — at the
200 message limit, new messages don't change the count. Now compares
last message timestamp instead.
2. WS handler only triggered on type 'message' — observer-decoded
GRP_TXT packets broadcast as type 'packet' were missed. Now also
triggers refresh on packet events with GRP_TXT payload type.