Skip to content
This repository was archived by the owner on Jan 23, 2026. It is now read-only.

Commit c06b69b

Browse files
frahlgclaude
andcommitted
Fix metric tracking and simplify UI
- Metric tracking now auto-creates wildcard patterns from topics (e.g., telemetry/device123/meter -> telemetry/+/meter) - Tracked metrics will match ALL similar topics, not just exact matches - Remove confusing schema change notifications - Cleaner stats panel without schema clutter 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 38912c9 commit c06b69b

3 files changed

Lines changed: 46 additions & 65 deletions

File tree

README.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ A high-performance MQTT explorer and debug tool built in Rust for the Sourceful
3434
- **Metric tracking with sparklines** - Track numeric fields over time with live graphs
3535
- **MQTT wildcard filters** - Filter topics using `+` and `#` patterns
3636
- **Latency monitoring** - Track message delays and jitter
37-
- **Schema change detection** - Alerts when JSON payload structure changes
3837
- **Starred topics** - Bookmark important topics with persistence
3938
- **Clipboard support** - Copy topics and payloads
4039
- **JSON syntax highlighting** - Pretty-printed payload inspection
@@ -223,8 +222,7 @@ Real-time statistics including:
223222
- Message count and rate
224223
- Data throughput
225224
- **Latency metrics** - Message delays, inter-arrival times, jitter
226-
- **Schema changes** - Alerts when JSON structure changes (+field, -field, ~type)
227-
- Topic counts by category
225+
- Topic counts by category (Sourceful entities)
228226
- Device health summary
229227
- Tracked metrics with sparklines
230228

src/app.rs

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crossterm::event::{KeyCode, KeyModifiers};
55
use crate::config::Config;
66
use crate::mqtt::{ConnectionState, MqttEvent, MqttMessage};
77
use crate::persistence::UserData;
8-
use crate::state::{get_numeric_fields, ChangeType, DeviceTracker, LatencyTracker, MessageBuffer, MetricTracker, SchemaTracker, Stats, TopicInfo, TopicTree};
8+
use crate::state::{get_numeric_fields, DeviceTracker, LatencyTracker, MessageBuffer, MetricTracker, SchemaTracker, Stats, TopicInfo, TopicTree};
99
use crate::state::metric_tracker::topic_matches;
1010

1111
/// Current UI panel focus
@@ -213,17 +213,8 @@ impl App {
213213
self.device_tracker.process_message(&msg.topic, msg.payload_size());
214214
// Process for latency tracking
215215
self.latency_tracker.record_message(&msg.payload);
216-
// Process for schema change detection
217-
let schema_changes = self.schema_tracker.process_message(&msg.topic, &msg.payload);
218-
if !schema_changes.is_empty() {
219-
let change = &schema_changes[0];
220-
let msg = match change.change_type {
221-
ChangeType::FieldAdded => format!("Schema +{}", change.field_path),
222-
ChangeType::FieldRemoved => format!("Schema -{}", change.field_path),
223-
ChangeType::TypeChanged => format!("Schema ~{}", change.field_path),
224-
};
225-
self.set_status(&msg);
226-
}
216+
// Process for schema tracking (silent - no notifications)
217+
let _ = self.schema_tracker.process_message(&msg.topic, &msg.payload);
227218
self.message_buffer.push(msg);
228219
}
229220
MqttEvent::StateChange(state) => {
@@ -257,14 +248,16 @@ impl App {
257248
KeyCode::Enter => {
258249
if let Some((field, _)) = self.available_fields.get(self.metric_select_index) {
259250
if let Some(topic) = &self.selected_topic {
260-
// Create a label for the metric
261-
let label = format!("{}", field);
251+
// Create a wildcard pattern to match similar topics
252+
// e.g., telemetry/device123/meter/zap/json -> telemetry/+/meter/+/json
253+
let pattern = create_wildcard_pattern(topic);
254+
let label = format!("{} ({})", field, short_topic(topic));
262255
self.metric_tracker.track(
263256
label.clone(),
264-
topic.clone(),
257+
pattern,
265258
field.clone(),
266259
);
267-
self.set_status(&format!("Tracking: {}", label));
260+
self.set_status(&format!("Tracking: {}", field));
268261
}
269262
}
270263
self.input_mode = InputMode::Normal;
@@ -798,3 +791,38 @@ impl App {
798791
}
799792
}
800793
}
794+
795+
/// Create a wildcard pattern from a specific topic
796+
/// Replaces segments that look like IDs with + wildcards
797+
/// e.g., "telemetry/zap-0000d8c467e385a0/meter/zap/json" -> "telemetry/+/meter/+/json"
798+
fn create_wildcard_pattern(topic: &str) -> String {
799+
topic
800+
.split('/')
801+
.map(|segment| {
802+
// Replace segments that look like device IDs or UUIDs
803+
if segment.len() > 8 && (
804+
segment.contains('-') || // UUIDs or device IDs like zap-0000d8c467e385a0
805+
segment.chars().all(|c| c.is_ascii_hexdigit()) || // Hex strings
806+
segment.starts_with("zap-") ||
807+
segment.starts_with("dev-") ||
808+
segment.parse::<u64>().is_ok() // Numeric IDs
809+
) {
810+
"+".to_string()
811+
} else {
812+
segment.to_string()
813+
}
814+
})
815+
.collect::<Vec<_>>()
816+
.join("/")
817+
}
818+
819+
/// Get a short version of a topic for display
820+
fn short_topic(topic: &str) -> String {
821+
let parts: Vec<&str> = topic.split('/').collect();
822+
if parts.len() <= 2 {
823+
topic.to_string()
824+
} else {
825+
// Show first and last parts
826+
format!("{}/..", parts[0])
827+
}
828+
}

src/ui/stats_view.rs

Lines changed: 1 addition & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use ratatui::{
77
};
88

99
use crate::app::{App, Panel};
10-
use crate::state::{render_sparkline, ChangeType, HealthStatus, LatencyTracker, Stats};
10+
use crate::state::{render_sparkline, HealthStatus, LatencyTracker, Stats};
1111
use super::bordered_block;
1212

1313
pub fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
@@ -179,51 +179,6 @@ pub fn render_stats(frame: &mut Frame, app: &App, area: Rect) {
179179
}
180180
}
181181

182-
// Schema changes (show if any recent changes)
183-
let recent_changes = app.schema_tracker.recent_changes();
184-
if !recent_changes.is_empty() {
185-
// Only show changes from last 60 seconds
186-
let recent: Vec<_> = recent_changes
187-
.iter()
188-
.filter(|c| c.timestamp.elapsed().as_secs() < 60)
189-
.collect();
190-
191-
if !recent.is_empty() {
192-
lines.push(Line::from(""));
193-
lines.push(Line::from(vec![
194-
Span::styled("Schema Changes", Style::default().add_modifier(Modifier::BOLD).fg(Color::Yellow)),
195-
]));
196-
197-
for change in recent.iter().take(3) {
198-
let (symbol, color) = match change.change_type {
199-
ChangeType::FieldAdded => ("+", Color::Green),
200-
ChangeType::FieldRemoved => ("-", Color::Red),
201-
ChangeType::TypeChanged => ("~", Color::Yellow),
202-
};
203-
204-
let field_display = if change.field_path.len() > 15 {
205-
format!("{}...", &change.field_path[..12])
206-
} else {
207-
change.field_path.clone()
208-
};
209-
210-
lines.push(Line::from(vec![
211-
Span::styled(format!(" {} ", symbol), Style::default().fg(color)),
212-
Span::styled(field_display, Style::default().fg(Color::White)),
213-
]));
214-
}
215-
216-
if recent.len() > 3 {
217-
lines.push(Line::from(vec![
218-
Span::styled(
219-
format!(" +{} more", recent.len() - 3),
220-
Style::default().fg(Color::DarkGray),
221-
),
222-
]));
223-
}
224-
}
225-
}
226-
227182
// Sourceful entity counts (if we tracked them)
228183
lines.push(Line::from(""));
229184
lines.push(Line::from(vec![

0 commit comments

Comments
 (0)