Skip to content

fix(meshcore): MQTT unread dot + readability + byte-accurate message counter#3790

Merged
Yeraze merged 4 commits into
mainfrom
claude/great-dijkstra-m4ga20
Jun 26, 2026
Merged

fix(meshcore): MQTT unread dot + readability + byte-accurate message counter#3790
Yeraze merged 4 commits into
mainfrom
claude/great-dijkstra-m4ga20

Conversation

@Yeraze

@Yeraze Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Fixes #3787 and closes #3782.

Summary

Bug #3787 — MQTT notification dot persists when MQTT messages are hidden

Root cause: /api/messages/unread-counts counted all unread messages regardless of the showMqttMessages toggle, so even when MQTT messages were hidden from the UI, they still contributed to the unread badge.

Fix:

  • Added excludeMqtt query parameter to /api/messages/unread-counts
  • MessagingContext now reads showMqttMessages from UIContext and passes excludeMqtt: !showMqttMessages to useUnreadCounts
  • Backend filters using OR (viaMqtt IS NULL OR viaMqtt = false) — includes older records where the column didn't exist yet
  • Fixed provider ordering in App.tsx: UIProvider now wraps MessagingProvider so UIContext is available inside MessagingContext

Also in #3787 — timestamp/hop-count text readability:

  • Timestamp opacity: 0.70.85
  • Received-message timestamp color: --ctp-subtext0 (doubly muted) → --ctp-subtext1
  • Hop count non-clickable opacity: 0.70.85

Feature #3782 — Byte-accurate MeshCore message counter

Adds a live counter below the MeshCore send bar that shows <bytes used> / <limit>, tracking UTF-8 encoded bytes to match what the firmware actually enforces (multi-byte emoji/CJK characters count more than 1).

Per-context limits:

Context Limit
Channel 130 bytes
Channel + scope 120 bytes
Direct message 150 bytes

Counter appears only when the draft is non-empty. Color is neutral → yellow at 90% → red when over limit. Send button disabled when over limit. Backend updated to use Buffer.byteLength(text, 'utf8') with the same per-context limits.

Test plan

  • MQTT messages hidden: verify no unread badge appears for MQTT-only messages when "Show MQTT messages" is off
  • MQTT messages shown: verify unread badge counts correctly when MQTT shown
  • Older messages (pre-viaMqtt column, NULL values) show in unread count normally
  • Timestamp text is noticeably more readable on both sent (dark) and received (light) bubbles
  • MeshCore channel: counter appears, turns yellow near 130 bytes, red over 130, send disabled
  • MeshCore channel with scope active: limit is 120 bytes
  • MeshCore DM: limit is 150 bytes
  • Sending a message at exactly the limit succeeds; one byte over is rejected by backend with a clear error
  • Multi-byte characters (emoji, CJK) count correctly in the byte counter

🤖 Generated with Claude Code

https://claude.ai/code/session_01TpTiMaXqnhkW6TqNxWGMg5


Generated by Claude Code

claude added 2 commits June 26, 2026 13:56
…/hop text readability (#3787)

Two fixes for issue #3787:

1. MQTT notification dot: The unread-count badge was counting MQTT-relayed
   messages even when showMqttMessages=false, causing a phantom dot. Added
   excludeMqtt query param to /api/messages/unread-counts, threaded the
   showMqttMessages toggle from UIContext through MessagingContext and
   useUnreadCounts hook. Provider order in App.tsx fixed so UIProvider
   wraps MessagingProvider (UIContext must be available inside Messaging).

2. Text readability: Timestamp and hop-count metadata opacity bumped from
   0.7 to 0.85; received-message timestamps use --ctp-subtext1 instead of
   the doubly-muted --ctp-subtext0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpTiMaXqnhkW6TqNxWGMg5
…ts (#3782)

Adds a live byte counter below the MeshCore message input that counts
UTF-8 encoded bytes (not characters) to accurately reflect what the
firmware enforces. Limits are context-aware:

- Channel message: 130 bytes
- Channel message with scope: 120 bytes (scope header consumes ~10 bytes)
- Direct message: 150 bytes

Counter appears only when the draft is non-empty. Color shifts to yellow
at 90% of the limit and red when over. Send button is disabled when the
limit is exceeded. Backend validation also uses Buffer.byteLength(text, 'utf8')
with the same per-context limits instead of the previous character-count heuristic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpTiMaXqnhkW6TqNxWGMg5
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review — fix(meshcore): MQTT unread dot + readability + byte-accurate message counter

Overall: Well-structured PR with two clear problem/solution pairs and good multi-database awareness. A few points worth attention:


Bug #3787 — MQTT unread dot

✅ What's good

  • Provider ordering fix (UIProvider wrapping MessagingProvider) is the correct solution; useUI() failing silently would have been worse.
  • The OR (viaMqtt IS NULL OR viaMqtt = false) filter in getUnreadCountsByChannelAsync (src/db/repositories/notifications.ts:852) correctly handles pre-column NULL rows.
  • queryKey includes excludeMqtt (src/hooks/useUnreadCounts.ts:66), so toggling the setting correctly invalidates the cache.

⚠️ Points to address

1. SQLite legacy path ignores excludeMqtt

database.ts has a legacy sync getUnreadCountsByChannel method (line ~8063) that still uses raw SQL. It branches to this path for SQLite and builds a mqttClause string, but the newer async path in notificationsRepo.getUnreadCountsByChannelAsync uses Drizzle. Both paths are wired through getUnreadCountsByChannelAsync (which checks isSQLite() and falls back to the sync method for SQLite). Confirm the Drizzle path in notifications.ts:832 is actually the path reached on SQLite, or the filter silently does nothing for most deployments.

2. DM unread counts not filtered

excludeMqtt is applied only to channel counts (getUnreadCountsByChannelAsync). DM counts (getBatchUnreadDMCountsAsync) don't accept or apply excludeMqtt. This is probably intentional (MQTT bridge rarely generates DMs), but if a bridged DM arrives it will still show a badge even when MQTT is hidden. Worth a comment acknowledging this scope decision.

3. No test for the new query parameter

The existing meshcoreRoutes.test.ts only has 'should send message with valid text' hitting the send route. There are no tests for:

  • GET /api/messages/unread-counts?excludeMqtt=true filtering correctly
  • The UIProviderMessagingProvider ordering (regression test for the provider order bug)

Feature #3782 — Byte-accurate message counter

✅ What's good

  • TextEncoder on the client (MeshCoreMessageStream.tsx:170) and Buffer.byteLength on the server (meshcoreRoutes.ts:210) are the correct way to measure UTF-8 byte lengths — these agree.
  • Constants are DRY and defined in one place (VALIDATION object in meshcoreRoutes.ts:69-98).
  • The counter is hidden when draft is empty (MeshCoreMessageStream.tsx:355) — good UX.
  • Backend validates with the same per-context limits as the frontend.

⚠️ Points to address

4. maxBytes prop hardcoded to magic numbers in consumer components

MeshCoreChannelsView.tsx:614-620 computes the limit inline with bare numbers (120, 130). While these match VALIDATION.MAX_MESSAGE_BYTES_CHANNEL*, they're not imported from a shared constant. If the limits ever change, two places need updating. Consider exporting the constants or a helper from meshcoreRoutes.ts (or a shared types file).

5. Frontend scope logic and backend scope logic can diverge

The frontend sets maxBytes=120 when showScopeOverride && overrideScope !== null && overrideScope !== '' OR when resolvedScope is truthy (MeshCoreChannelsView.tsx:614-619). The backend sets scoped-limit when typeof scope === 'string' && scope.trim().length > 0 (meshcoreRoutes.ts:1498). These are equivalent today, but if the frontend sends an override scope of '' (explicit unscoped), the frontend uses 120 bytes while the backend uses 130. This edge case could cause confusing "send disabled" states if the override field is opened and cleared.

6. TextEncoder instantiated on every render

// MeshCoreMessageStream.tsx:170
const byteLen = useMemo(() => new TextEncoder().encode(draft).length, [draft]);

new TextEncoder() is cheap but encode() allocates a new Uint8Array on every keystroke. For large drafts this is fine in practice, but TextEncoder.prototype.encodeInto with a pre-allocated buffer, or simply using .length comparison after checking if any char code > 127 exists, would be more efficient. Not a blocking issue for this feature.


Readability fixes (CSS/HopCountDisplay)

The opacity changes (0.7→0.85) and color token change (subtext0→subtext1) in messages.css and HopCountDisplay.tsx are small, well-targeted, and follow the existing token convention. No issues.


Summary

Area Status
Provider ordering fix ✅ Correct
excludeMqtt backend filter ✅ Correct for Postgres/MySQL; verify SQLite routing
DM unread counts ⚠️ Not filtered (needs comment or intentional exclusion doc)
Byte counter — frontend ✅ Correct; minor allocation note
Byte counter — backend ✅ Correct
Scope limit edge case ⚠️ Off-by-one possible when override cleared to ''
Magic numbers in consumer ⚠️ Minor DRY concern
Test coverage ⚠️ No new tests for excludeMqtt param or provider ordering

The core logic is sound. The main asks before merging are: (1) confirm the SQLite path actually reaches the Drizzle code, and (2) add at least one test for the excludeMqtt query parameter.

@Yeraze

Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Reviewed while untangling the overlapping #3787 PRs (closed the redundant #3789; #3774 stays on #3786). This PR is the keeper for #3787 + #3782 — the approach is right and CI is green. One concrete gap worth addressing before merge:

Unread filtering covers the sidebar dot but not the per-channel badge.

There are two unread-count paths:

  1. Sidebar Channels-icon dotMessagingContextuseUnreadCounts/api/messages/unread-counts. ✅ This PR correctly threads excludeMqtt here.
  2. Per-channel unread badge inside ChannelsTab (ChannelsTab.tsx:874 unread-badge) → fed by the /poll aggregator (App.tsx:2589 builds newUnreadCounts from data.unreadCounts.channels; also useServerData.ts:140-141). ⚠️ This path still calls getUnreadCountsByChannelAsync(... ) without excludeMqtt (server.ts:~2991).

Net effect: with "Show MQTT/Bridge Messages" unchecked, the sidebar dot correctly stays dark for MQTT-only traffic, but a channel's numeric badge can still count those hidden MQTT messages — an inconsistent dot-vs-badge state.

Suggest threading the same excludeMqtt (derived from showMqttMessages) through the /poll unread aggregation so both surfaces agree.

Other than that: provider reorder is safe (UIContext doesn't consume MessagingContext), readability bumps look good, and the #3782 byte-counter is consistent client+server with the test updated. Minor process nit: this bundles two issues (#3787 + #3782) in one PR.

…le (#3787)

The dedicated unread query already applies excludeMqtt, fixing the sidebar
dot. But the per-channel unread badges in ChannelsTab are fed by the /poll
aggregate, which is not filtered — so with MQTT hidden the dot went dark
while a channel badge could still count MQTT-only traffic.

Source the badge counts from the already-filtered unread query
(unreadCountsData) instead of the raw poll payload, bridged into the
memoized poll callback via a ref. Both unread surfaces now agree.

Adds a NotificationsRepository test asserting excludeMqtt drops viaMqtt
rows from the channel unread count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review — fix(meshcore): MQTT unread dot + readability + byte-accurate message counter

Tasks:

  • Gather context from PR description and prior comments
  • Read changed files
  • Review code quality, bugs, performance, security, test coverage
  • Post final review

Overall Assessment

The core logic is sound and well-structured. Several issues raised in earlier reviews have been addressed — the ref-based bridge for the poll path (unreadCountsDataRef at App.tsx:537) cleanly resolves Yeraze's gap about per-channel badges being out of sync with the sidebar dot. One SQLite concern from the previous automated review can be closed: getUnreadCountsByChannelAsync at database.ts:8141 calls the legacy sync method which does handle excludeMqtt via mqttClause (line 8075). The filter works for all three backends.

A few items still worth attention before merge:


Bug #3787 — MQTT unread dot / per-channel badge

✅ Resolved

  • SQLite path (database.ts:8141) routes through the sync legacy method which correctly builds AND (m.viaMqtt IS NULL OR m.viaMqtt = 0) — filter applies for all backends.
  • Per-channel badge now reads from unreadCountsDataRef.current?.channels (the filtered query) and only falls back to data.unreadCounts?.channels (the raw poll) until the first dedicated fetch lands (App.tsx:2604).
  • queryKey includes excludeMqtt (useUnreadCounts.ts:66) — toggling the setting invalidates the cache correctly.

⚠️ Remaining items

1. Race condition on initial render — fallback to unfiltered poll counts

App.tsx:2604:

const filteredChannelUnreads = unreadCountsDataRef.current?.channels ?? data.unreadCounts?.channels;

On the very first poll response, unreadCountsDataRef.current is undefined (the useUnreadCounts query hasn't returned yet). The code falls back to data.unreadCounts?.channels, which is the unfiltered poll aggregate. This means the per-channel badge will briefly show MQTT-inflated counts before the dedicated query resolves (~10 s default interval). Not a correctness bug (it self-corrects), but noticeable during initial page load when "Show MQTT messages" is off.

2. DM unread counts not filtered — needs a comment

getBatchUnreadDMCountsAsync has no excludeMqtt parameter. This is probably fine in practice (MQTT bridges rarely generate DMs), but when a bridged DM does arrive it will show a badge despite MQTT being hidden. A one-line comment in the code acknowledging this intentional scope omission would prevent future confusion.

3. No route-level test for excludeMqtt query parameter

The test coverage added (notifications.test.ts:723) validates the repository method correctly. There's no test for the HTTP route (/api/messages/unread-counts?excludeMqtt=true) to verify the query parameter is parsed and threaded through. Given the routing is simple, this is a low-risk gap, but it means a typo in the Express handler (e.g. req.query.excludeMqtt !== 'true') would go undetected.


Feature #3782 — Byte-accurate MeshCore message counter

✅ Good

  • new TextEncoder().encode(draft).length (MeshCoreMessageStream.tsx:170) and Buffer.byteLength(text, 'utf8') (meshcoreRoutes.ts:210) are equivalent and correct for UTF-8.
  • Counter is hidden when draft is empty (MeshCoreMessageStream.tsx:355) — good UX default.
  • VALIDATION object in meshcoreRoutes.ts:69–98 is a clean single source of truth for the backend.
  • Backend validates with the same per-context limits as the frontend.

⚠️ Points to address

4. Magic numbers in consumer components — DRY gap

MeshCoreChannelsView.tsx:614–619 hardcodes 120 and 130 inline. MeshCoreDirectMessagesView.tsx:399 hardcodes 150. These are orphaned from the VALIDATION constants in meshcoreRoutes.ts. Since VALIDATION is in a server-only file, these can't be imported directly on the client. The cleanest fix is a small shared constants file (e.g. src/types/meshcoreValidation.ts) exporting MESHCORE_BYTE_LIMITS that both sides import. If the channel limit changes, today's code requires updating three files (server route + two view components).

5. Scope edge case: overrideScope === '' causes frontend/backend mismatch

MeshCoreChannelsView.tsx:615:

showScopeOverride && overrideScope !== null && overrideScope !== ''
  ? 120
  : resolvedScope
  ? 120
  : 130

meshcoreRoutes.ts:1498:

const hasScope = typeof scope === 'string' && scope.trim().length > 0;

If a user opens the scope override field (setting showScopeOverride = true) then clears it back to '':

  • Frontend: overrideScope !== '' is false, falls through to resolvedScope ? 120 : 130 — likely 130 since no resolved scope.
  • Backend: receives scope = '' from the POST body, scope.trim().length > 0 is false → uses 130 bytes.
    These happen to agree in this case, but the logic path is brittle. The backend guard scope.trim().length > 0 is defensive; the frontend multi-ternary would benefit from a helper function to avoid the implicit triple-branch logic.

6. useMemo allocation on every keystroke

MeshCoreMessageStream.tsx:170:

const byteLen = useMemo(() => new TextEncoder().encode(draft).length, [draft]);

useMemo is the right hook here (recomputes only when draft changes), but encode() allocates a Uint8Array on every character typed. For large messages this is negligible (max 150 bytes), but TextEncoder.prototype.encodingInto with a pre-allocated buffer, or a simpler check, would be more efficient. Non-blocking for this PR but worth a future note.


Readability fixes (CSS / HopCountDisplay)

messages.css opacity changes (0.7→0.85) and HopCountDisplay.tsx color token change (--ctp-subtext0→--ctp-subtext1) are small, focused, and follow the Catppuccin token convention. No issues.


Summary

Area Status
SQLite excludeMqtt routing ✅ Confirmed working
Provider ordering fix ✅ Correct
Per-channel badge poll fallback ⚠️ Brief MQTT-inflated flash on initial load
DM unread counts ⚠️ Unfiltered — needs a comment
Backend excludeMqtt filter ✅ Correct (all backends)
Repository test for excludeMqtt ✅ Added
HTTP route test for excludeMqtt param ⚠️ Missing
Byte counter client/server agreement ✅ Correct
Magic numbers in consumer components ⚠️ DRY gap (shared constant file recommended)
Scope edge case ('' override) ⚠️ Works today but brittle
Readability CSS/token bumps ✅ Clean

Items 1 and 3 are the only ones that affect observable behavior. The rest are maintainability/documentation gaps. The implementation is solid enough to merge with those acknowledged.

@Yeraze

Yeraze commented Jun 26, 2026

Copy link
Copy Markdown
Owner Author

Addressed the gap I flagged above in 1c8e4476.

The per-channel unread badge now also respects "Show MQTT/Bridge Messages": instead of reading the unfiltered /poll aggregate, the badge counts are now sourced from the same excludeMqtt-filtered unread query that drives the sidebar dot (bridged into the memoized poll callback via a ref, with the poll payload kept only as a pre-first-fetch fallback). No /poll backend change was needed, and the poll queryKey is left untouched (so WebSocket setQueryData/cache reads are unaffected).

Added a NotificationsRepository test asserting excludeMqtt drops viaMqtt rows from the channel unread count (passes on SQLite locally; PG/MySQL covered by the same parametrized suite in CI).

Monitoring CI; will merge once green.

…oser (#3782)

The MeshCore composer counter reimplemented UTF-8 byte counting, the
90%/100% warning thresholds, the N/M display format, and the warning/over
colors inline — all of which already exist in src/utils/text.ts
(getUtf8ByteLength + formatByteCount) and the shared .byte-counter* CSS used
by the Meshtastic channel and DM composers.

Reuse those helpers and the shared color classes. Per-context limits stay
consumer-specific (Meshtastic 200 default; MeshCore channel 130 / scoped 120
/ DM 150, passed via maxBytes). Only the layout is overridden — MeshCore
shows the counter as a block under the input+button row rather than the
absolute overlay the Meshtastic textarea uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Yeraze's task —— View job


PR Review — fix(meshcore): MQTT unread dot + readability + byte-accurate message counter

Tasks:

  • Gather context from PR description and prior comments
  • Read changed files
  • Review code quality, bugs, performance, security, test coverage
  • Post final review

Overall Assessment

This is a solid PR with two well-scoped fixes. Previous automated reviews were thorough and Yeraze's follow-up commit (1c8e4476) correctly addressed the per-channel badge gap. The implementation is sound. I have a few remaining observations after reading the full diff:


Bug #3787 — MQTT Unread Filtering

✅ What's working

  • SQLite path confirmed correct: database.ts:8063 builds AND (m.viaMqtt IS NULL OR m.viaMqtt = 0) in raw SQL for SQLite. The async wrapper at database.ts:8139-8146 routes SQLite through this sync method, so all three database backends apply the filter correctly.
  • Per-channel badge fixed: App.tsx:2604 now reads from unreadCountsDataRef.current?.channels (the filtered query result) and only falls back to the poll aggregate before the first dedicated fetch resolves. Clean approach.
  • Provider ordering fix: UIProvider now wraps MessagingProvider so useUI() is available inside MessagingContext.tsx:49 when it reads showMqttMessages.
  • Repository test added: notifications.test.ts:723-743 validates the excludeMqtt behavior at the DB layer.

⚠️ Minor remaining items

1. Race window on initial page load

App.tsx:2604:

const filteredChannelUnreads = unreadCountsDataRef.current?.channels ?? data.unreadCounts?.channels;

On first render, unreadCountsDataRef.current is undefined until useUnreadCounts resolves (~10s interval). During this window the badge falls back to the unfiltered /poll aggregate — users with "Show MQTT messages" off will briefly see MQTT-inflated badge counts. This is a cosmetic flicker, not a correctness bug, and self-corrects. Worth a // TODO: comment here, but not blocking.

2. DM unread counts remain unfiltered — needs acknowledgment

getBatchUnreadDMCountsAsync has no excludeMqtt parameter (server.ts:3009). If an MQTT bridge sends a DM, it shows a badge even when MQTT is hidden. This scope decision is likely intentional (rare case) but there's no comment acknowledging it. Adding a one-liner here would prevent future reviewers from reopening this.

3. No HTTP route-level test for excludeMqtt

The repository layer is tested, but GET /api/messages/unread-counts?excludeMqtt=true has no route test. The query parameter parsing at server.ts:2603 (req.query.excludeMqtt === 'true') is simple but a typo there would go silently undetected.


Feature #3782 — Byte-accurate MeshCore Message Counter

✅ What's working

  • Client/server byte counting agrees: getUtf8ByteLength in src/utils/text.ts:8-13 uses TextEncoder.encode().length; isValidMessage in meshcoreRoutes.ts:210 uses Buffer.byteLength(text, 'utf8'). Both produce identical UTF-8 byte counts for all inputs.
  • Shared utility: the counter reuses the existing getUtf8ByteLength/formatByteCount helpers from src/utils/text.ts that the Meshtastic channel and DM composers already use. Good DRY practice.
  • UX details correct: counter hidden when draft empty (MeshCoreMessageStream.tsx:361), send button disabled when over limit (MeshCoreMessageStream.tsx:356), warning at 90% and red over limit via byte-counter-warning/byte-counter-over classes.
  • Backend VALIDATION constants: well-organized and documented in meshcoreRoutes.ts:69-98.
  • CSS override approach: .meshcore-byte-counter.byte-counter overrides the shared .byte-counter overlay layout to a block display — clean use of specificity without duplicating styles.

⚠️ Minor items

4. Magic numbers in consumer components — DRY gap

MeshCoreChannelsView.tsx:614-620 hardcodes 120 and 130 inline. MeshCoreDirectMessagesView.tsx:399 hardcodes 150. These orphaned constants don't track VALIDATION in meshcoreRoutes.ts (a server-side file). If limits change, three files need updating. The shared src/utils/text.ts or a new src/types/meshcoreValidation.ts would be a better home.

5. TextEncoder instantiated on every call

src/utils/text.ts:8-13:

export function getUtf8ByteLength(text: string): number {
  const encoder = new TextEncoder();
  return encoder.encode(text).length;
}

A module-level const encoder = new TextEncoder() would avoid re-instantiation on every keystroke. Minor in practice (max 150 bytes encoded), but easy to fix. Additionally, encode() allocates a Uint8Array — using encoder.encodeInto(text, buffer) with a pre-allocated buffer would avoid that allocation entirely. Non-blocking.

6. Scope edge-case: overrideScope === '' with showScopeOverride = true

MeshCoreChannelsView.tsx:615:

showScopeOverride && overrideScope !== null && overrideScope !== ''
  ? 120
  : resolvedScope ? 120 : 130

meshcoreRoutes.ts:1498:

const hasScope = typeof scope === 'string' && scope.trim().length > 0;

If a user opens the scope override field then clears it to '', frontend uses 130 (no scope) and backend also uses 130 (scope.trim().length === 0 → false). These agree today, but the triple-branch frontend ternary is fragile — a future change to what gets sent in the POST body could create a mismatch that's hard to debug.


Readability Fixes (#3787)

The opacity changes (0.7→0.85) in messages.css:246,256 and the color token change (--ctp-subtext0→--ctp-subtext1) for received-message timestamps are well-targeted and follow the Catppuccin token convention. HopCountDisplay.tsx:94,106 opacity changes are consistent. No issues.


Summary

Area Status
SQLite excludeMqtt routing ✅ Confirmed working via sync path
Provider ordering (UIProviderMessagingProvider) ✅ Correct
Per-channel badge filtering ✅ Fixed via unreadCountsDataRef
Sidebar dot filtering ✅ Fixed
Backend filter (all 3 DBs) ✅ Correct
Repository test for excludeMqtt ✅ Added and well-scoped
Initial-load badge flash (MQTT-inflated) ⚠️ Cosmetic, self-corrects
DM unread counts unfiltered ⚠️ Needs a comment (intentional scope)
HTTP route test for excludeMqtt param ⚠️ Not present
Byte counter (client/server agreement) ✅ Correct
TextEncoder re-instantiation ⚠️ Minor — module-level singleton preferred
Magic numbers in view components ⚠️ DRY gap; consider a shared constants file
Scope edge-case ('' override) ⚠️ Works today but brittle
Readability CSS/token changes ✅ Clean

The implementation is correct and ready to merge. The remaining items are either cosmetic (initial-load flash), documentation (DM exclusion comment), or maintainability concerns that can be addressed in follow-up issues rather than blocking this PR.

@Yeraze Yeraze merged commit 035eae5 into main Jun 26, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Channels behavior wrong when MQTT ignored and time/hop text is hard to read [FEAT] MeshCore: Show used and remaining allowed characters in input line

2 participants