|
| 1 | +# OtelContext UI Redesign — Design Spec |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Redesign the OtelContext UI to be more intuitive and professional. The service map becomes the hero view, showing stats and flow across all services. Must handle 100-200 services performantly and neatly. Remove low-value pages, consolidate stats into fewer, richer views. |
| 6 | + |
| 7 | +## Navigation (4 Views) |
| 8 | + |
| 9 | +| View | Purpose | Replaces | |
| 10 | +|------|---------|----------| |
| 11 | +| **Service Map** | Hero view — hierarchical topology with per-service stats, flow edges, side panel detail | Overview, Services, Metrics | |
| 12 | +| **Traces** | Trace list + waterfall span detail | (kept, minor cleanup) | |
| 13 | +| **Logs** | Live log stream + search + similarity | (kept, minor cleanup) | |
| 14 | +| **MCP Console** | JSON-RPC tool surface | (kept as-is) | |
| 15 | + |
| 16 | +### Removed Pages |
| 17 | + |
| 18 | +- **Overview** — KPI stats merged into Service Map top bar |
| 19 | +- **Metrics** — traffic/latency/service-map metrics merged into Service Map side panel |
| 20 | +- **Archive** — dedicated page removed; archive stats shown in top bar; archive search available via MCP Console tools |
| 21 | + |
| 22 | +## Service Map View (Primary) |
| 23 | + |
| 24 | +### Layout Structure |
| 25 | + |
| 26 | +``` |
| 27 | +┌─────────────────────────────────────────────────────────────────┐ |
| 28 | +│ TopNav: Logo │ [Service Map] [Traces] [Logs] [MCP] │ Stats Bar │ |
| 29 | +├─────────────────────────────────────────────────────────────────┤ |
| 30 | +│ Search │ Edge threshold slider │ Zoom controls (+/−/Fit) │ |
| 31 | +├──────────────────────────────────────────────┬──────────────────┤ |
| 32 | +│ │ Side Panel │ |
| 33 | +│ Hierarchical Service Map │ (on click) │ |
| 34 | +│ (ECharts canvas) │ │ |
| 35 | +│ │ - KPI cards │ |
| 36 | +│ GATEWAY tier │ - Health score │ |
| 37 | +│ API LAYER tier │ - Connections │ |
| 38 | +│ SERVICES tier │ - Recent errors │ |
| 39 | +│ DATA tier │ - Action links │ |
| 40 | +│ │ │ |
| 41 | +│ [minimap] │ │ |
| 42 | +└──────────────────────────────────────────────┴──────────────────┘ |
| 43 | +``` |
| 44 | + |
| 45 | +### Top Stats Bar (Global) |
| 46 | + |
| 47 | +Always-visible horizontal strip in the top nav showing: |
| 48 | +- Total services (with healthy count) |
| 49 | +- Total traces |
| 50 | +- Total logs |
| 51 | +- Error rate (colored red if >5%) |
| 52 | +- DB size (`DBSizeMB` from RepoStats) — serves as storage indicator since archive size is not tracked separately |
| 53 | +- WebSocket connection indicator |
| 54 | + |
| 55 | +Data sources: `GET /api/metrics/dashboard` + `GET /api/stats` |
| 56 | + |
| 57 | +**Auto-refresh:** Dashboard stats poll every 30 seconds to keep top bar current. |
| 58 | + |
| 59 | +### Hierarchical Layout (Tier-based) |
| 60 | + |
| 61 | +Services arranged in tiers from top to bottom, representing request flow direction: |
| 62 | + |
| 63 | +| Tier | Criteria | Visual | |
| 64 | +|------|----------|--------| |
| 65 | +| **Gateway** | Nodes with no inbound edges (entry points) | Top row | |
| 66 | +| **API Layer** | Nodes called directly by gateway | Second row | |
| 67 | +| **Services** | Internal services (middle of graph) | Third row | |
| 68 | +| **Data** | Leaf nodes with no outbound edges (databases, caches) | Bottom row | |
| 69 | + |
| 70 | +**Tier assignment algorithm:** |
| 71 | +1. Build adjacency maps (inbound and outbound) from edges |
| 72 | +2. BFS from all root nodes (no inbound edges), assigning each node tier = longest path from any root |
| 73 | +3. Identify leaf nodes (no outbound edges) — override their tier to max tier (Data layer) |
| 74 | +4. Bucket tiers into 4 display rows: tier 0 = Gateway, tier 1 = API Layer, tier 2..max-1 = Services, tier max = Data |
| 75 | +5. If a graph has no root nodes (cycles only), fall back to sorting by `span_count` descending and distributing evenly across 4 tiers |
| 76 | + |
| 77 | +**Layout engine:** Compute tier positions in pure JS (BFS), then pass fixed `(x, y)` coordinates to ECharts graph nodes. Layout is computed once on data load (not animated per-frame). No external layout library needed. |
| 78 | + |
| 79 | +### Service Nodes (Compact) |
| 80 | + |
| 81 | +Each node renders as a small card: |
| 82 | + |
| 83 | +``` |
| 84 | +┌──────────────────┐ |
| 85 | +│ ● service-name │ |
| 86 | +│ 340 rps 0.2% err│ |
| 87 | +└──────────────────┘ |
| 88 | +``` |
| 89 | + |
| 90 | +- **Health dot**: green (#22c55e), orange (#fb923c), red (#ef4444) based on `status` field |
| 91 | +- **Service name**: truncated at ~16 chars, full name on hover tooltip |
| 92 | +- **Two metrics**: RPS (from `request_rate_rps`) + error rate (from `error_rate`) |
| 93 | +- **Background tint**: subtle color matching health status |
| 94 | +- **Border**: colored by health status |
| 95 | + |
| 96 | +Node sizing: |
| 97 | +- Normal (< 70 services): ~120x46px |
| 98 | +- Dense (70+ services): ~90x36px, smaller font |
| 99 | + |
| 100 | +### Edge Rendering (Progressive Disclosure) |
| 101 | + |
| 102 | +Edges represent service-to-service calls. With 200 services, there could be 500-1000+ edges. |
| 103 | + |
| 104 | +**Default behavior:** |
| 105 | +- Show only edges with `call_count` above a configurable threshold |
| 106 | +- Default threshold: `max(median call_count, 10)` — ensures meaningful filtering even in sparse graphs where median is very low |
| 107 | +- User can adjust via a slider in the toolbar (range: 1 to max call_count) |
| 108 | + |
| 109 | +**Edge visual properties:** |
| 110 | +- Width: scaled by `log(call_count)` — range 1px to 4px |
| 111 | +- Color: matches health status of the edge (green/yellow/red based on `error_rate`) |
| 112 | +- Opacity: higher traffic = more opaque |
| 113 | +- Direction: arrows pointing from source to target (top to bottom flow) |
| 114 | + |
| 115 | +**Edge culling for performance:** |
| 116 | +- Maximum 500 rendered edges at any time |
| 117 | +- Below threshold edges are hidden, not removed from data |
| 118 | +- Hovering a node temporarily shows all its edges regardless of threshold |
| 119 | + |
| 120 | +### Side Panel (Service Detail) |
| 121 | + |
| 122 | +Slides in from the right when a service node is clicked. Width: 280px. Contents: |
| 123 | + |
| 124 | +**Header:** |
| 125 | +- Health dot + service name + status badge (HEALTHY/DEGRADED/CRITICAL) |
| 126 | + |
| 127 | +**KPI Cards (2x2 grid):** |
| 128 | +- RPS (`request_rate_rps`) |
| 129 | +- Error Rate (`error_rate`) — colored red if > 5% |
| 130 | +- Avg Latency (`avg_latency_ms`) |
| 131 | +- P99 Latency (`p99_latency_ms`) |
| 132 | + |
| 133 | +**Health Score Bar:** |
| 134 | +- Visual bar 0-1.0 with numeric label |
| 135 | +- Color gradient: red → yellow → green |
| 136 | + |
| 137 | +**Connected Services:** |
| 138 | +- **Upstream** — services that call this one (inbound edges), with call counts |
| 139 | +- **Downstream** — services this one calls (outbound edges), with call counts |
| 140 | +- Each entry is clickable → pans the map to that node and opens its side panel (replacing the current one) |
| 141 | + |
| 142 | +**Alerts / Recent Errors:** |
| 143 | +- Display the `alerts` array from the `SystemNode` (already available in graph response — e.g., "error rate above 5%", "avg latency above 500ms") |
| 144 | +- These are computed server-side during graph generation, no additional API call needed |
| 145 | + |
| 146 | +**Action Links:** |
| 147 | +- "View Traces →" — navigates to Traces view, pre-filtered by `service_name` |
| 148 | +- "View Logs →" — navigates to Logs view, pre-filtered by `service_name` |
| 149 | + |
| 150 | +### Search |
| 151 | + |
| 152 | +Text input that filters/highlights services by name. As user types: |
| 153 | +- Matching nodes get highlighted (bright border/glow) |
| 154 | +- Non-matching nodes dim (lower opacity) |
| 155 | +- Map pans to center on first match |
| 156 | + |
| 157 | +### Minimap |
| 158 | + |
| 159 | +Small overview in bottom-right corner (~100x80px). Implemented as a second ECharts instance with `silent: true` (no interactions except click-to-navigate), rendering the same graph data at thumbnail scale with simplified nodes (dots only, no labels). A semi-transparent rectangle overlay shows the current viewport position. On click, the main map pans to the corresponding position. |
| 160 | + |
| 161 | +### Zoom Controls |
| 162 | + |
| 163 | +Three buttons in toolbar: |
| 164 | +- **+** Zoom in |
| 165 | +- **−** Zoom out |
| 166 | +- **⊡** Fit all (reset zoom to show entire graph) |
| 167 | + |
| 168 | +ECharts built-in zoom/pan via mouse scroll and drag. |
| 169 | + |
| 170 | +## Traces View |
| 171 | + |
| 172 | +Keep current two-column layout with minor cleanup: |
| 173 | +- Left: trace list (25 limit, click to select) |
| 174 | +- Right: selected trace waterfall + detail |
| 175 | +- Add service filter support (pre-filter when navigating from Service Map) |
| 176 | +- Remove JsonViewer at bottom (raw JSON adds clutter, not useful for most users) |
| 177 | + |
| 178 | +## Logs View |
| 179 | + |
| 180 | +Keep current two-column layout with minor cleanup: |
| 181 | +- Left: search + severity filter + similarity results |
| 182 | +- Right: live log stream (WebSocket) |
| 183 | +- Add service filter support (pre-filter when navigating from Service Map) |
| 184 | + |
| 185 | +## MCP Console |
| 186 | + |
| 187 | +Keep as-is. No changes needed — it's already well-designed for its purpose. |
| 188 | + |
| 189 | +## Cross-View Navigation |
| 190 | + |
| 191 | +When clicking "View Traces →" or "View Logs →" from the Service Map side panel, the service name is passed via React state in `App.tsx`: |
| 192 | + |
| 193 | +```typescript |
| 194 | +// App.tsx state |
| 195 | +const [activeView, setActiveView] = useState<OtelView>('services'); |
| 196 | +const [serviceFilter, setServiceFilter] = useState<string | null>(null); |
| 197 | + |
| 198 | +// Navigation from side panel |
| 199 | +const navigateToTraces = (serviceName: string) => { |
| 200 | + setServiceFilter(serviceName); |
| 201 | + setActiveView('traces'); |
| 202 | +}; |
| 203 | +``` |
| 204 | + |
| 205 | +`TracesPage` and `LogsPage` receive `serviceFilter` as a prop and use it to pre-filter their initial data fetch. A "clear filter" chip is shown when a filter is active, allowing the user to remove it. |
| 206 | + |
| 207 | +## Data Refresh Strategy |
| 208 | + |
| 209 | +| View | Refresh Mechanism | Interval | |
| 210 | +|------|-------------------|----------| |
| 211 | +| Top Stats Bar | Polling `useDashboard()` | 30 seconds | |
| 212 | +| Service Map | Polling `useSystemGraph()` | 60 seconds (matches backend GraphRAG refresh loop) | |
| 213 | +| Logs | WebSocket real-time stream | Continuous | |
| 214 | +| Traces | Manual refresh button | On demand | |
| 215 | +| MCP Console | On demand | Per tool call | |
| 216 | + |
| 217 | +Service Map refresh preserves current zoom/pan position and selected service. Layout is only recomputed if the node set changes (new/removed services), not on metric updates. |
| 218 | + |
| 219 | +## Technical Implementation |
| 220 | + |
| 221 | +### Dependencies |
| 222 | + |
| 223 | +No new dependencies required. Continue using: |
| 224 | +- **ECharts** — canvas rendering for the service map (already installed) |
| 225 | +- **Radix UI** — dialogs, tooltips (already installed) |
| 226 | +- **Lucide React** — icons (already installed) |
| 227 | + |
| 228 | +Layout is computed in pure JS via BFS tier assignment — no external layout library needed. |
| 229 | + |
| 230 | +### Performance Strategy (100-200 Services) |
| 231 | + |
| 232 | +| Technique | Purpose | |
| 233 | +|-----------|---------| |
| 234 | +| ECharts canvas renderer | All nodes/edges rendered to single canvas, not DOM | |
| 235 | +| Tier layout computed once | No per-frame physics simulation (BFS runs once on data load) | |
| 236 | +| Edge threshold culling | Max 500 visible edges | |
| 237 | +| Semantic zoom | At zoom < 0.6: hide metric text on nodes, show only health dot + name. At zoom < 0.3: collapse nodes to colored dots only. Implemented via ECharts `graphScaleLimit` + zoom event listener toggling label visibility | |
| 238 | +| Dense mode auto-detection | Smaller nodes when count > 70 | |
| 239 | +| `React.memo` on side panel | Prevent re-render on map pan/zoom | |
| 240 | +| Debounced search | Filter during typing, not on every keystroke | |
| 241 | + |
| 242 | +### Data Flow |
| 243 | + |
| 244 | +``` |
| 245 | +App.tsx |
| 246 | +├── useSystemGraph() → Service Map nodes + edges |
| 247 | +├── useDashboard() → Top bar stats (traces, logs, errors, services) |
| 248 | +├── useTraces() → Traces view |
| 249 | +├── useLogs() → Logs view |
| 250 | +├── useWebSocket() → Real-time log stream |
| 251 | +└── useMCP() → MCP Console |
| 252 | +``` |
| 253 | + |
| 254 | +The `useMetrics()` hook can be simplified or removed — the Service Map gets its data from `useSystemGraph()` which already includes per-service metrics and edge stats. |
| 255 | + |
| 256 | +### Files to Modify |
| 257 | + |
| 258 | +| File | Change | |
| 259 | +|------|--------| |
| 260 | +| `App.tsx` | Remove Overview/Metrics/Archive views, update nav | |
| 261 | +| `TopNav.tsx` | 4 nav items instead of 7, add global stats bar | |
| 262 | +| `ServicesPage.tsx` | Complete rewrite — hierarchical layout, side panel, search, edge threshold | |
| 263 | +| `TracesPage.tsx` | Add service filter prop, remove JsonViewer | |
| 264 | +| `LogsPage.tsx` | Add service filter prop | |
| 265 | +| `useSystemGraph.ts` | Add tier computation logic | |
| 266 | +| `types/api.ts` | Add `db_size_mb` to `RepoStats` interface, remove unused types | |
| 267 | + |
| 268 | +### Files to Delete |
| 269 | + |
| 270 | +| File | Reason | |
| 271 | +|------|--------| |
| 272 | +| `OverviewPage.tsx` | Merged into Service Map + top bar | |
| 273 | +| `MetricsPage.tsx` | Merged into Service Map side panel | |
| 274 | +| `ArchivePage.tsx` | Stats in top bar, search via MCP | |
| 275 | +| `useArchive.ts` | No longer needed | |
| 276 | +| `useMetrics.ts` | No longer needed (data comes from useSystemGraph) | |
| 277 | +| `JsonViewer.tsx` | No remaining consumers after removing OverviewPage and TracesPage usage | |
| 278 | + |
| 279 | +### Color System |
| 280 | + |
| 281 | +Health status colors (consistent across nodes, edges, badges): |
| 282 | +- **Healthy**: `#22c55e` (green) — background tint `#0f2618`, border `#166534` |
| 283 | +- **Degraded**: `#fb923c` (orange) — background tint `#1a1207`, border `#854d0e` (matches existing codebase convention) |
| 284 | +- **Critical**: `#ef4444` (red) — background tint `#1c0707`, border `#991b1b` |
| 285 | +- **Data stores**: `#818cf8` (indigo) — background tint `#0c0c1f`, border `#312e81` |
| 286 | + |
| 287 | +### Tier Label Styling |
| 288 | + |
| 289 | +Vertical text labels on the left edge of the map canvas: |
| 290 | +- Font: 9px uppercase |
| 291 | +- Color: `#3f3f46` (subtle zinc) |
| 292 | +- Labels: GATEWAY, API LAYER, SERVICES, DATA |
0 commit comments