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

feat: add new SVG icons and refactor ConfigProvider to use React Query for configuration fetching#17

Merged
andersonleal merged 1 commit intomainfrom
feat/trace-improvments
Feb 20, 2026
Merged

feat: add new SVG icons and refactor ConfigProvider to use React Query for configuration fetching#17
andersonleal merged 1 commit intomainfrom
feat/trace-improvments

Conversation

@andersonleal
Copy link
Copy Markdown
Contributor

@andersonleal andersonleal commented Feb 20, 2026

Pull Request

Summary

This PR introduces trace UI improvements, new branding assets, and refactors configuration fetching to use React Query across the iii-console frontend.

Key changes

  • New SVG icons: Added iii-black1.1.svg and iii-white1.1.svg to public/ for branding
  • ConfigProvider refactor: Replaced manual useState/useEffect config fetching with TanStack React Query (useQuery), including retry logic and simplified loading/error states
  • Trace improvements: Enhanced FlameGraph, WaterfallChart, TraceMap, TraceFilters, and AttributesFilter components with improved layout and filtering
  • Route refactors: Updated functions, triggers, logs, states, streams, and traces routes for consistency and better UX
  • UI components: Minor updates to card, json-viewer, TimeRangeFilter, and Sidebar components

Type of change

  • New feature (non-breaking change which adds functionality)
  • Refactoring (no functional changes)
  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • Code follows project style guidelines (Biome)
  • Self-review of code performed
  • Documentation updated (if applicable)
  • No new warnings introduced

Additional context

  • 26 files changed, ~2,015 additions, ~840 deletions
  • ConfigProvider now uses queryKey: ['console-config'] with retry: true and retryDelay: 3000
  • Trace components received significant layout and filter enhancements

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced flame graph with interactive zoom, pan, minimap, and color-mode toggles (status/service).
    • Resizable waterfall chart columns and span panels for better data visibility.
    • Split trace and span views with draggable dividers; auto-collapse when span selected.
    • Expanded navigation menu including Streams, Traces, Logs, and Config pages.
  • Improvements

    • Better keyboard navigation (Escape key support for menus and modals).
    • Enriched tooltip information displaying duration, self-time, and trace percentages.
    • Improved accessibility with proper label-input associations and button semantics.
    • Better error handling and messaging throughout the app.
  • Bug Fixes

    • Fixed potential crashes from missing metadata and stats.
    • Improved list rendering stability with enhanced React key handling.

…y for configuration fetching

- Introduced `iii-black1.1.svg` and `iii-white1.1.svg` for UI enhancements.
- Refactored `ConfigProvider` to utilize `useQuery` from React Query for fetching console configuration, improving error handling and state management.
- Updated various components to enhance accessibility and maintainability, including adding IDs to input elements and improving state management in filters and charts.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 20, 2026

📝 Walkthrough

Walkthrough

This PR refactors state management across the console frontend, replacing scattered useState hooks with reducer-based machines for improved organization in routes (functions, logs, states, streams, triggers, traces). It introduces interactive enhancements to trace visualization (FlameGraph with minimap, zoom, selection), accessibility improvements (labels, button types, Escape-key handling), and adjusts API integration patterns (React Query for config, safer null handling). Multiple components also export new types and update public signatures.

Changes

Cohort / File(s) Summary
State Management Refactors
src/routes/functions.tsx, src/routes/logs.tsx, src/routes/states.tsx, src/routes/streams.tsx, src/routes/triggers.tsx
Replaced multiple useState hooks with dedicated useReducer-based state machines for UI state, pagination, filtering, and invocation concerns; updated event handlers and JSX to dispatch actions instead of calling setters directly.
Trace Visualization Enhancements
src/components/traces/FlameGraph.tsx, src/components/traces/WaterfallChart.tsx, src/routes/traces.tsx
FlameGraph: Added interactive view state (hover, tooltip, color mode, zoom, pan), minimap, selection highlighting, and enhanced toolbar. WaterfallChart: Introduced display state reducer and span column resizing with localStorage persistence. TraceView: Added resizable split-panel layout for trace and span views with draggable dividers.
Trace Filter & Detail Components
src/components/traces/TraceFilters.tsx, src/components/traces/AttributesFilter.tsx, src/components/traces/SpanErrorsTab.tsx, src/components/traces/SpanLogsTab.tsx, src/components/traces/TraceMap.tsx
TraceFilters: Updated onFilterChange signature to use keyof TraceFilterState and refactored temp input state with reducer. AttributesFilter: Switched to ID-based DraftEntry model for stable key management. ErrorsTab & LogsTab: Changed React key generation for better uniqueness. TraceMap: Made node generation safe against missing stats.
Config & API Layer
src/api/config-provider.tsx, src/routes/config.tsx
Replaced in-component fetch logic in ConfigProvider with useQuery (React Query) for automatic retries and state management; added explicit button type attributes for form safety.
Layout & Navigation
src/components/layout/Sidebar.tsx, src/components/filters/TimeRangeFilter.tsx, src/routes/index.tsx
Sidebar: Removed Dashboard nav, switched to alias imports, replaced IIILogo with image tags, added Escape-key and accessibility improvements. TimeRangeFilter: Added label-input associations and interactive overlay with Escape handling. Index route: Redirects to /functions and simplified error presentation.
UI Components & Utilities
src/components/ui/card.tsx, src/components/ui/json-viewer.tsx, src/api/flows.ts, src/lib/traceTransform.ts, src/main.tsx
CardTitle: Updated to explicitly render children. JsonViewer: Added button type attributes and refactored array key generation. Flows & TraceTransform: Added safe null-coalescing for metadata and depth lookups. Main: Reordered provider nesting and added root element null check.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through state machines dance,
Reducers hoist where useState once pranced,
FlameGraphs now zoom, and panels can slide—
With split-panel views and filters worldwide,
The console runs cleaner, with fewer bugs in trance! 🔥

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding SVG icons and refactoring ConfigProvider to use React Query, which are the primary focuses evident throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/trace-improvments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/console-frontend/src/routes/states.tsx (1)

231-251: ⚠️ Potential issue | 🟡 Minor

Bug: handleEditItem incorrectly uses addModal reducer for saving state.

The handleEditItem function dispatches START_SAVE and SAVE_DONE to dispatchAddModal instead of tracking save state for edits separately. This causes the modal's saving state to change during edit operations, which could lead to incorrect UI behavior if the add modal is open during an edit (unlikely but possible), and is semantically incorrect.

Consider adding a saving field to EditState or creating a separate loading state.

🔧 Proposed fix - extend EditState
 interface EditState {
   editingItem: string | null
   editValue: string
+  saving: boolean
 }
 type EditAction =
   | { type: 'OPEN_EDIT'; itemKey: string; value: string }
   | { type: 'SET_EDIT_VALUE'; value: string }
   | { type: 'CLOSE_EDIT' }
+  | { type: 'START_EDIT_SAVE' }
+  | { type: 'EDIT_SAVE_DONE' }

-const editInitial: EditState = { editingItem: null, editValue: '' }
+const editInitial: EditState = { editingItem: null, editValue: '', saving: false }

 function editReducer(state: EditState, action: EditAction): EditState {
   switch (action.type) {
     case 'OPEN_EDIT':
-      return { editingItem: action.itemKey, editValue: action.value }
+      return { editingItem: action.itemKey, editValue: action.value, saving: false }
     case 'SET_EDIT_VALUE':
       return { ...state, editValue: action.value }
     case 'CLOSE_EDIT':
-      return { editingItem: null, editValue: '' }
+      return { editingItem: null, editValue: '', saving: false }
+    case 'START_EDIT_SAVE':
+      return { ...state, saving: true }
+    case 'EDIT_SAVE_DONE':
+      return { ...state, saving: false }
     default:
       return state
   }
 }

Then update handleEditItem:

   const handleEditItem = async (item: StateItem) => {
     if (!selectedGroupId) return

-    dispatchAddModal({ type: 'START_SAVE' })
+    dispatchEdit({ type: 'START_EDIT_SAVE' })
     try {
       // ... existing logic ...
       dispatchEdit({ type: 'CLOSE_EDIT' })
       refetchItems()
     } catch {
       // Handle error
     } finally {
-      dispatchAddModal({ type: 'SAVE_DONE' })
+      dispatchEdit({ type: 'EDIT_SAVE_DONE' })
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/states.tsx` around lines 231 - 251,
handleEditItem is incorrectly toggling the add-modal saving state via
dispatchAddModal({ type: 'START_SAVE' }) / 'SAVE_DONE'; change it to track
saving on the edit reducer instead: add a saving boolean to EditState (or a
separate editLoading flag) and dispatch edit actions (e.g., dispatchEdit({ type:
'START_SAVE' }) and dispatchEdit({ type: 'SAVE_DONE' })) inside handleEditItem
around the async setStateItem call, and remove the dispatchAddModal calls so the
add modal saving state is not affected.
packages/console-frontend/src/routes/index.tsx (1)

29-43: ⚠️ Potential issue | 🟠 Major

The route unconditionally redirects, making DashboardPage unreachable dead code.

The loader (line 41) always throws a redirect to /functions, so the DashboardPage component (lines 194–663) will never render. This creates ~470 lines of unused code. Either remove the component and related code, or make the redirect conditional if the dashboard should be accessible.

Note: The Promise.allSettled() prefetch (lines 34–40) is not awaited, so it fires asynchronously after the redirect is thrown—the prefetch likely won't complete before navigation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/index.tsx` around lines 29 - 43, The
loader for createFileRoute('/') always throws redirect({ to: '/functions' }) so
DashboardPage is never rendered; either remove the DashboardPage component and
its related code, or make the redirect conditional in the loader (e.g., check a
flag/user state or query param) and only redirect when appropriate; if you keep
the loader prefetch logic, await
Promise.allSettled([...queryClient.prefetchQuery(statusQuery),
queryClient.prefetchQuery(functionsQuery()),
queryClient.prefetchQuery(triggersQuery()),
queryClient.prefetchQuery(streamsQuery()),
queryClient.prefetchQuery(metricsHistoryQuery(100))]) before redirecting so
prefetch completes, and ensure you return appropriate loader data instead of
unconditionally throwing from loader when DashboardPage must be reachable.
🧹 Nitpick comments (15)
packages/console-frontend/src/components/ui/card.tsx (1)

27-37: Good fix: CardTitle now correctly renders children.

The change from a self-closing <h3 {...props} /> to explicit <h3>{children}</h3> is correct—spreading props on a self-closing tag doesn't render children in JSX.

Pre-existing type mismatch: The ref type is HTMLParagraphElement but the element is <h3> (HTMLHeadingElement). Consider aligning the types:

🔧 Suggested fix for type consistency
 export const CardTitle = React.forwardRef<
-  HTMLParagraphElement,
+  HTMLHeadingElement,
   React.HTMLAttributes<HTMLHeadingElement>
 >(({ className, children, ...props }, ref) => (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/ui/card.tsx` around lines 27 - 37,
The CardTitle forwardRef has a type mismatch: the ref is declared as
HTMLParagraphElement while the component renders an <h3> (a heading). Update the
generics and ref type on CardTitle to use HTMLHeadingElement and
React.HTMLAttributes<HTMLHeadingElement> so the ref and props match the rendered
<h3> element (update the forwardRef signature and ref generic accordingly).
packages/console-frontend/src/components/traces/TraceMap.tsx (1)

100-117: Defensive null check is good, but angle calculation has a subtle issue if filtering ever occurs.

The refactor to .map().filter() with type narrowing is idiomatic. However, since serviceNames comes from serviceMap.keys() (line 94), serviceMap.get(name) will always return a defined value—the null check is purely defensive.

The subtle issue: angles are computed using index i and nodeCount from the pre-filter array. If a node were ever filtered out, the circular layout would have gaps instead of redistributing evenly. This is currently unreachable but would become a bug if the logic ever changes.

♻️ Option: recalculate angles after filtering for correctness
-    const nodes: ServiceNode[] = serviceNames
-      .map((name, i) => {
-        const stats = serviceMap.get(name)
-        if (!stats) return null
-        const angle = (i / nodeCount) * 2 * Math.PI - Math.PI / 2
-
-        return {
-          id: name,
-          name,
-          spanCount: stats.spanCount,
-          totalDuration: stats.totalDuration,
-          errorCount: stats.errorCount,
-          x: centerX + radius * Math.cos(angle),
-          y: centerY + radius * Math.sin(angle),
-          color: getServiceColor(name),
-        }
-      })
-      .filter((n): n is ServiceNode => n !== null)
+    const validServices = serviceNames
+      .map((name) => ({ name, stats: serviceMap.get(name) }))
+      .filter((s): s is { name: string; stats: NonNullable<typeof s.stats> } => s.stats != null)
+
+    const nodes: ServiceNode[] = validServices.map(({ name, stats }, i) => {
+      const angle = (i / validServices.length) * 2 * Math.PI - Math.PI / 2
+      return {
+        id: name,
+        name,
+        spanCount: stats.spanCount,
+        totalDuration: stats.totalDuration,
+        errorCount: stats.errorCount,
+        x: centerX + radius * Math.cos(angle),
+        y: centerY + radius * Math.sin(angle),
+        color: getServiceColor(name),
+      }
+    })

Alternatively, since the null case is unreachable, you could simplify by using the non-null assertion (serviceMap.get(name)!) with a comment explaining the invariant—though the current defensive approach is reasonable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/traces/TraceMap.tsx` around lines
100 - 117, The angle calculation for nodes uses the pre-filter index i and
nodeCount which can produce gaps if any items are filtered out; update
TraceMap.tsx so angles are computed after filtering: first build the filtered
list of valid services (use serviceMap.get(name) to filter and produce a
ServiceNode-like partial), then map that filtered array with its own index to
compute angle using its length as the nodeCount, or alternatively assert
non-null with serviceMap.get(name)! and remove the defensive filter (add a
comment explaining the invariant). Ensure you update references to nodes,
serviceNames, nodeCount, and the angle computation to use the post-filter
array/length so layout evenly redistributes.
packages/console-frontend/src/components/filters/TimeRangeFilter.tsx (1)

318-330: Static ID could cause duplicate IDs if multiple instances are rendered.

The hardcoded id="time-range-start" will create invalid HTML if multiple TimeRangeFilter components are mounted simultaneously. Consider using React.useId() to generate unique IDs per instance.

♻️ Suggested fix using useId

Add to imports and component body:

-import { useCallback, useMemo, useState } from 'react'
+import { useCallback, useId, useMemo, useState } from 'react'

Inside the component:

 export function TimeRangeFilter({
   ...
 }: TimeRangeFilterProps) {
+  const id = useId()
   const [isOpen, setIsOpen] = useState(false)

Then use dynamic IDs:

-                    htmlFor="time-range-start"
+                    htmlFor={`${id}-start`}
                     ...
-                    id="time-range-start"
+                    id={`${id}-start`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx` around
lines 318 - 330, The label/input pair in the TimeRangeFilter component uses a
hardcoded id ("time-range-start") which can collide when multiple instances
mount; import and call React.useId() (or useId from React) inside the
TimeRangeFilter component to create a unique base id (e.g., idBase = useId())
and replace the static id and htmlFor with a scoped id like `${idBase}-start`
(and similarly update any matching end input if present) so the Input's id and
the label's htmlFor remain unique per component instance while keeping the
existing value/onChange (customStart, setCustomStart) intact.
packages/console-frontend/src/api/config-provider.tsx (2)

38-44: Consider setting staleTime and reviewing retry behavior.

The config data is unlikely to change during a session. Setting staleTime: Infinity (or a large value) would prevent unnecessary refetches. Also, retry: true enables infinite retries—if that's intentional, it's fine; otherwise, consider a finite number like retry: 3.

Suggested configuration
   const { data: config, error } = useQuery({
     queryKey: ['console-config'],
     queryFn: fetchConsoleConfig,
     retry: true,
     retryDelay: 3000,
+    staleTime: Infinity,
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/api/config-provider.tsx` around lines 38 - 44,
The useQuery in ConfigProvider is currently missing a staleTime and uses retry:
true (infinite retries); update the query options in ConfigProvider (the
useQuery call) to set staleTime: Infinity (or a large ms value) to avoid
refetching session-static console config, and change retry from true to a finite
count such as retry: 3 (or an appropriate number) to prevent infinite retry
loops.

33-35: Side effect inside queryFn may cause unintended behavior.

Calling setConfig(data) inside the query function means it runs on every successful fetch, including refetches and retries. If the config module caches state, this is fine. However, if setConfig has side effects (e.g., triggering re-renders or persisting to storage), consider moving it to an onSuccess callback pattern or ensuring idempotency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/api/config-provider.tsx` around lines 33 - 35,
The query function currently calls setConfig(data) after parsing the response,
causing side effects on every fetch/retry; move the state mutation out of the
query function and into an onSuccess callback (or the component/caller that
invokes useQuery) so the queryFn only returns the fetched ConsoleConfig and
setConfig is invoked once or guarded for idempotency; specifically, remove the
setConfig(data) call from the fetch logic in config-provider.tsx and instead
call setConfig from the useQuery onSuccess handler (or wrap setConfig to no-op
when the value is unchanged) so setConfig and any re-render/persistence side
effects are controlled.
packages/console-frontend/src/routes/traces.tsx (1)

284-317: Consider throttling resize updates with requestAnimationFrame.

The resize handler updates state on every mousemove event, which can fire many times per second and cause unnecessary re-renders. The WaterfallChart component uses RAF throttling for its resize logic.

Example throttling pattern
   useEffect(() => {
+    let rafId: number | null = null
     const onMouseMove = (e: MouseEvent) => {
       if (!isResizingRef.current) return
+      if (rafId !== null) return
+      rafId = requestAnimationFrame(() => {
+        rafId = null
+        if (!isResizingRef.current) return
         const dx = resizeStartRef.current.x - e.clientX
         // ... rest of logic
+      })
     }
     const onMouseUp = () => {
       if (isResizingRef.current) {
+        if (rafId !== null) {
+          cancelAnimationFrame(rafId)
+          rafId = null
+        }
         isResizingRef.current = null
         // ...
       }
     }
     // ...
+    return () => {
+      // ... cleanup
+      if (rafId !== null) cancelAnimationFrame(rafId)
+    }
   }, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/traces.tsx` around lines 284 - 317, The
mousemove handler currently calls setPanelWidths on every event causing
excessive re-renders; throttle updates using requestAnimationFrame by adding an
RAF loop inside the useEffect: in onMouseMove (used with isResizingRef and
resizeStartRef) capture the desired width calculation (using clampPanelWidth,
PANEL_MIN_WIDTH, totalWidth, etc.) into a local "next" value and schedule a
single requestAnimationFrame to call setPanelWidths(trace/ span) if one isn’t
already pending; store the raf id in a ref so you can cancel it on cleanup and
when onMouseUp clears isResizingRef and cancels any pending RAF to avoid
orphaned frames.
packages/console-frontend/src/components/traces/WaterfallChart.tsx (1)

270-284: Use role="separator" instead of role="slider" for resize handles.

A resize handle is semantically a separator that can be moved, not a slider. The aria-orientation should also be "vertical" since dragging is horizontal but it separates content vertically.

Suggested fix
           <div
-            role="slider"
+            role="separator"
             aria-label="Resize span column"
-            aria-orientation="horizontal"
+            aria-orientation="vertical"
             aria-valuemin={150}
             aria-valuemax={600}
             aria-valuenow={spanColWidth}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx` around
lines 270 - 284, In WaterfallChart.tsx update the resize handle element: change
role="slider" to role="separator", set aria-orientation="vertical" (since it
separates columns), and remove the slider-specific attributes aria-valuemin,
aria-valuemax and aria-valuenow; keep tabIndex, the event handlers
(startColResize, onDoubleClick that calls setSpanColWidth(300)), title and
styling so keyboard/mouse accessibility is preserved.
packages/console-frontend/src/routes/functions.tsx (1)

226-229: Consider including HTTP status in the result.

The InvocationResult interface defines status?: number (line 30), but it's never populated when setting the result. If invokeFunctionApi returns a status code, consider including it for debugging purposes.

If status is available from the API
       dispatchInvocation({
         type: 'SET_RESULT',
-        result: { success: result.success, duration, data: result.data, error: result.error },
+        result: { success: result.success, status: result.status, duration, data: result.data, error: result.error },
       })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/functions.tsx` around lines 226 - 229,
The dispatchInvocation call that sets the invocation result is missing the HTTP
status field defined on InvocationResult; update the object passed to
dispatchInvocation (inside the function that calls invokeFunctionApi and
currently uses dispatchInvocation({ type: 'SET_RESULT', result: { success:
result.success, duration, data: result.data, error: result.error } })) to
include status (e.g., status: result.status or the response HTTP status returned
by invokeFunctionApi) so the SET_RESULT payload populates
InvocationResult.status for better debugging.
packages/console-frontend/src/routes/logs.tsx (1)

285-302: Consider resetting page to 1 when filters change.

When searchQuery or activeLevelFilters change, the filteredLogs array may shrink, but currentPage isn't reset. This can result in an empty page view if the user was on page 5 and the filtered results only have 2 pages.

Example fix using useEffect
+  // Reset to page 1 when filters change
+  useEffect(() => {
+    dispatchPagination({ type: 'SET_PAGE', payload: 1 })
+  }, [searchQuery, activeLevelFilters.size, timeRange, selectedSeverity])
+
   const filteredLogs = useMemo(() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/logs.tsx` around lines 285 - 302, When
filters change the current page can become invalid; add an effect to reset
pagination by calling setCurrentPage(1) whenever searchQuery or
activeLevelFilters change. Implement a useEffect that depends on [searchQuery,
activeLevelFilters] (or [searchQuery, activeLevelFilters, logs] if you prefer)
and inside call setCurrentPage(1); reference the existing filteredLogs,
currentPage and setCurrentPage state variables in the file to locate where to
add the effect.
packages/console-frontend/src/routes/triggers.tsx (2)

467-467: Unused underscore variable in filter callback.

The underscore in Object.entries(queryParams).filter(([_, v]) => v) could be replaced with a more explicit pattern or use .filter(([, v]) => v) to avoid the unused variable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/triggers.tsx` at line 467, The filter
callback for building queryEntries uses an unused named variable `_` which is
unnecessary; update the Object.entries call that produces queryEntries to
destructure only the used second element (e.g., use ([, v]) or similar) so the
unused key is not declared, i.e., change Object.entries(queryParams).filter(([_,
v]) => v) to use the ignored-first-element pattern and keep the filter logic
(reference: queryEntries and queryParams).

155-176: Missing default case in triggersUiReducer.

Unlike httpRequestReducer and invokeReducer, the triggersUiReducer lacks a default case. While TypeScript's exhaustiveness checking should catch unhandled actions at compile time, adding a default return ensures runtime safety.

🔧 Proposed fix
     case 'SET_EVENT_PAYLOAD':
       return { ...state, eventPayload: action.payload }
+    default:
+      return state
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/triggers.tsx` around lines 155 - 176,
triggersUiReducer is missing a default branch so unrecognized action types can
return undefined at runtime; update the switch in function triggersUiReducer to
include a default case that returns the current state (e.g., return state) to
ensure runtime safety for unknown TriggersUiAction values, and optionally
include an exhaustive type guard (using a never/assertUnreachable pattern) if
you want compile-time checks in addition to the default return.
packages/console-frontend/src/components/traces/FlameGraph.tsx (3)

93-100: Self-time calculation may be incorrect for overlapping child spans.

The current algorithm sums all children's durations and subtracts from parent duration. This assumes children execute sequentially without overlap. For concurrent/parallel spans, this could produce incorrect (potentially negative, though clamped to 0) self-time values.

If children can overlap in time, consider computing self-time based on actual time intervals rather than summing durations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/traces/FlameGraph.tsx` around lines
93 - 100, The self-time logic for FlameGraph is wrong for overlapping children:
instead of summing child durations in the loop over spanMap.values(), compute
the total covered time by merging child time intervals (use each
child.span.start_ms and child.span.duration_ms to form [start,end), clamp those
intervals to the parent span bounds, sort and merge overlapping intervals to get
the union length), then set node.selfTime = Math.max(0, node.span.duration_ms -
coveredUnionLength). Update the block that currently accumulates
childrenDuration to perform interval clipping/merging for node.children and
subtract the merged coverage from node.span.duration_ms.

427-435: Wheel panning also lacks upper bound check.

Similar to minimap click, horizontal panning via wheel has no maximum bound, allowing users to pan past the content.

🔧 Proposed fix
     } else {
-      dispatch({ type: 'SET_PAN', panOffset: Math.max(0, panOffset + e.deltaY) })
+      const container = containerRef.current
+      if (!container) return
+      const width = container.clientWidth
+      const graphWidth = (width - PADDING * 2) * zoomLevel
+      const maxOffset = Math.max(0, graphWidth - (width - PADDING * 2))
+      dispatch({ type: 'SET_PAN', panOffset: Math.max(0, Math.min(maxOffset, panOffset + e.deltaY)) })
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/traces/FlameGraph.tsx` around lines
427 - 435, handleWheel allows panOffset to grow without an upper bound; clamp
the new pan value before dispatching SET_PAN. In handleWheel, compute the
maximum allowable pan (e.g. maxPan = Math.max(0, totalContentWidth -
viewportWidth) or call the existing getMaxPan helper if present), then replace
Math.max(0, panOffset + e.deltaY) with a clamped value like Math.min(maxPan,
Math.max(0, panOffset + e.deltaY)) and dispatch that clamped value for 'SET_PAN'
so horizontal wheel panning cannot move past the content.

437-451: Minimap click panning could exceed valid bounds.

The newOffset calculation can produce values that pan beyond the graph's content when clicking near the edges of the minimap. While Math.max(0, newOffset) prevents negative values, there's no upper bound check.

🔧 Proposed fix to add upper bound
     // Center viewport on click position
     const clickPercent = (clickX - PADDING) / (width - PADDING * 2)
     const newOffset = clickPercent * graphWidth - (width - PADDING * 2) / 2
-    dispatch({ type: 'SET_PAN', panOffset: Math.max(0, newOffset) })
+    const maxOffset = graphWidth - (width - PADDING * 2)
+    dispatch({ type: 'SET_PAN', panOffset: Math.max(0, Math.min(maxOffset, newOffset)) })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/traces/FlameGraph.tsx` around lines
437 - 451, The minimap click handler (handleMinimapClick) currently only clamps
negative pan values but can exceed the maximum scrollable range; compute the
viewportWidth as (width - PADDING * 2), compute graphWidth as already done, then
compute maxOffset = Math.max(0, graphWidth - viewportWidth) and clamp newOffset
into [0, maxOffset] before dispatching SET_PAN via dispatch({ type: 'SET_PAN',
panOffset: clampedOffset }); use existing minimapRef, containerRef, PADDING and
zoomLevel symbols to locate the values to compute the clamp.
packages/console-frontend/src/routes/streams.tsx (1)

130-192: Consider extracting a MAX_MESSAGES constant.

The 1000 cap is a magic number inside the reducer; a named constant makes it easier to tune and reuse.

♻️ Suggested refactor
+const MAX_MESSAGES = 1000
+
 function messagesReducer(state: MessagesState, action: MessagesAction): MessagesState {
   switch (action.type) {
     case 'add_message': {
       const { entry } = action
       return {
-        messages: [entry, ...state.messages].slice(0, 1000),
+        messages: [entry, ...state.messages].slice(0, MAX_MESSAGES),
         stats: {
           ...state.stats,
           totalMessages: state.stats.totalMessages + 1,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/streams.tsx` around lines 130 - 192,
Introduce a named constant MAX_MESSAGES (e.g., const MAX_MESSAGES = 1000) near
the top of this module and replace the hard-coded 1000 in messagesReducer (the
.slice(0, 1000) call) with MAX_MESSAGES; ensure any future uses (if needed)
reference MAX_MESSAGES and keep INITIAL_MESSAGES_STATE unchanged, exporting
MAX_MESSAGES if it needs to be reused elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx`:
- Around line 390-400: The overlay's onKeyDown won't fire because it isn't
focusable; modify TimeRangeFilter to add a document-level keydown listener via
useEffect that watches isOpen and, when isOpen is true, attaches a handler that
calls setIsOpen(false) on Escape and removes the listener on cleanup (or when
isOpen becomes false); remove the onKeyDown prop from the overlay <div> and keep
the click handler for click-away behavior while ensuring the useEffect
references setIsOpen and isOpen so the Escape key reliably closes the panel.

In `@packages/console-frontend/src/components/layout/Sidebar.tsx`:
- Line 123: The img tag in Sidebar.tsx uses a hardcoded root-relative path
"/iii-white1.1.svg" (appears in the <img src=...> usage and again later) which
will break under a Vite base path; update both image src usages in the Sidebar
component to prefix the file with import.meta.env.BASE_URL (e.g., set src to
`${import.meta.env.BASE_URL}iii-white1.1.svg` or build the path via string
concatenation) so asset URLs respect the configured base path.

In `@packages/console-frontend/src/components/traces/SpanLogsTab.tsx`:
- Around line 55-57: The list item key in SpanLogsTab.tsx currently uses
`${event.name}-${event.timestamp}` which can collide for SpanEvent objects
lacking a unique id; update the key generation in the map rendering where you
use `event` (the map callback inside SpanLogsTab) to include the iteration index
(or another unique identifier if available) so keys become unique (e.g., combine
event.name, event.timestamp, and the map index) to prevent React key collisions
and mis-renders.

In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx`:
- Around line 141-144: The localStorage access in the spanColWidth initialiser
can throw (e.g., private mode), so update the useState initializer in
WaterfallChart.tsx to wrap localStorage.getItem('iii-span-col-width') in a
try-catch, parse the value only if retrieved and valid (fallback to 300 on any
error or NaN), and similarly wrap any future localStorage.setItem calls for
spanColWidth in try-catch to avoid runtime errors; reference the spanColWidth
and setSpanColWidth state variables to locate the code to change.

In `@packages/console-frontend/src/routes/index.tsx`:
- Around line 650-661: The Connection Issue UI block controlled by hasError in
routes/index.tsx is unreachable because the loader always redirects; remove or
relocate this error handling and implement it in the functions route instead.
Specifically, delete or disable the unreachable JSX that renders the yellow
"Connection Issue" card (the hasError conditional) and add equivalent error
handling and UI rendering in the /functions route's component or loader error
path (update the functions route loader or component to set and render an error
state). Ensure any state or props names (hasError) are wired to the functions
route logic so the UI becomes reachable when that route detects the engine
connection failure.

In `@packages/console-frontend/src/routes/streams.tsx`:
- Around line 456-458: The clearMessages handler currently clears messages and
resets selection but doesn't reset pagination, causing an empty view if the user
was on a later page; update the clearMessages function (where messagesDispatch
and setSelectedMessageId are called) to also reset pagination by calling
setCurrentPage(1) so the Pagination component (which slices filteredMessages by
currentPage and pageSize) shows page 1 immediately after clearing.

---

Outside diff comments:
In `@packages/console-frontend/src/routes/index.tsx`:
- Around line 29-43: The loader for createFileRoute('/') always throws
redirect({ to: '/functions' }) so DashboardPage is never rendered; either remove
the DashboardPage component and its related code, or make the redirect
conditional in the loader (e.g., check a flag/user state or query param) and
only redirect when appropriate; if you keep the loader prefetch logic, await
Promise.allSettled([...queryClient.prefetchQuery(statusQuery),
queryClient.prefetchQuery(functionsQuery()),
queryClient.prefetchQuery(triggersQuery()),
queryClient.prefetchQuery(streamsQuery()),
queryClient.prefetchQuery(metricsHistoryQuery(100))]) before redirecting so
prefetch completes, and ensure you return appropriate loader data instead of
unconditionally throwing from loader when DashboardPage must be reachable.

In `@packages/console-frontend/src/routes/states.tsx`:
- Around line 231-251: handleEditItem is incorrectly toggling the add-modal
saving state via dispatchAddModal({ type: 'START_SAVE' }) / 'SAVE_DONE'; change
it to track saving on the edit reducer instead: add a saving boolean to
EditState (or a separate editLoading flag) and dispatch edit actions (e.g.,
dispatchEdit({ type: 'START_SAVE' }) and dispatchEdit({ type: 'SAVE_DONE' }))
inside handleEditItem around the async setStateItem call, and remove the
dispatchAddModal calls so the add modal saving state is not affected.

---

Nitpick comments:
In `@packages/console-frontend/src/api/config-provider.tsx`:
- Around line 38-44: The useQuery in ConfigProvider is currently missing a
staleTime and uses retry: true (infinite retries); update the query options in
ConfigProvider (the useQuery call) to set staleTime: Infinity (or a large ms
value) to avoid refetching session-static console config, and change retry from
true to a finite count such as retry: 3 (or an appropriate number) to prevent
infinite retry loops.
- Around line 33-35: The query function currently calls setConfig(data) after
parsing the response, causing side effects on every fetch/retry; move the state
mutation out of the query function and into an onSuccess callback (or the
component/caller that invokes useQuery) so the queryFn only returns the fetched
ConsoleConfig and setConfig is invoked once or guarded for idempotency;
specifically, remove the setConfig(data) call from the fetch logic in
config-provider.tsx and instead call setConfig from the useQuery onSuccess
handler (or wrap setConfig to no-op when the value is unchanged) so setConfig
and any re-render/persistence side effects are controlled.

In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx`:
- Around line 318-330: The label/input pair in the TimeRangeFilter component
uses a hardcoded id ("time-range-start") which can collide when multiple
instances mount; import and call React.useId() (or useId from React) inside the
TimeRangeFilter component to create a unique base id (e.g., idBase = useId())
and replace the static id and htmlFor with a scoped id like `${idBase}-start`
(and similarly update any matching end input if present) so the Input's id and
the label's htmlFor remain unique per component instance while keeping the
existing value/onChange (customStart, setCustomStart) intact.

In `@packages/console-frontend/src/components/traces/FlameGraph.tsx`:
- Around line 93-100: The self-time logic for FlameGraph is wrong for
overlapping children: instead of summing child durations in the loop over
spanMap.values(), compute the total covered time by merging child time intervals
(use each child.span.start_ms and child.span.duration_ms to form [start,end),
clamp those intervals to the parent span bounds, sort and merge overlapping
intervals to get the union length), then set node.selfTime = Math.max(0,
node.span.duration_ms - coveredUnionLength). Update the block that currently
accumulates childrenDuration to perform interval clipping/merging for
node.children and subtract the merged coverage from node.span.duration_ms.
- Around line 427-435: handleWheel allows panOffset to grow without an upper
bound; clamp the new pan value before dispatching SET_PAN. In handleWheel,
compute the maximum allowable pan (e.g. maxPan = Math.max(0, totalContentWidth -
viewportWidth) or call the existing getMaxPan helper if present), then replace
Math.max(0, panOffset + e.deltaY) with a clamped value like Math.min(maxPan,
Math.max(0, panOffset + e.deltaY)) and dispatch that clamped value for 'SET_PAN'
so horizontal wheel panning cannot move past the content.
- Around line 437-451: The minimap click handler (handleMinimapClick) currently
only clamps negative pan values but can exceed the maximum scrollable range;
compute the viewportWidth as (width - PADDING * 2), compute graphWidth as
already done, then compute maxOffset = Math.max(0, graphWidth - viewportWidth)
and clamp newOffset into [0, maxOffset] before dispatching SET_PAN via
dispatch({ type: 'SET_PAN', panOffset: clampedOffset }); use existing
minimapRef, containerRef, PADDING and zoomLevel symbols to locate the values to
compute the clamp.

In `@packages/console-frontend/src/components/traces/TraceMap.tsx`:
- Around line 100-117: The angle calculation for nodes uses the pre-filter index
i and nodeCount which can produce gaps if any items are filtered out; update
TraceMap.tsx so angles are computed after filtering: first build the filtered
list of valid services (use serviceMap.get(name) to filter and produce a
ServiceNode-like partial), then map that filtered array with its own index to
compute angle using its length as the nodeCount, or alternatively assert
non-null with serviceMap.get(name)! and remove the defensive filter (add a
comment explaining the invariant). Ensure you update references to nodes,
serviceNames, nodeCount, and the angle computation to use the post-filter
array/length so layout evenly redistributes.

In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx`:
- Around line 270-284: In WaterfallChart.tsx update the resize handle element:
change role="slider" to role="separator", set aria-orientation="vertical" (since
it separates columns), and remove the slider-specific attributes aria-valuemin,
aria-valuemax and aria-valuenow; keep tabIndex, the event handlers
(startColResize, onDoubleClick that calls setSpanColWidth(300)), title and
styling so keyboard/mouse accessibility is preserved.

In `@packages/console-frontend/src/components/ui/card.tsx`:
- Around line 27-37: The CardTitle forwardRef has a type mismatch: the ref is
declared as HTMLParagraphElement while the component renders an <h3> (a
heading). Update the generics and ref type on CardTitle to use
HTMLHeadingElement and React.HTMLAttributes<HTMLHeadingElement> so the ref and
props match the rendered <h3> element (update the forwardRef signature and ref
generic accordingly).

In `@packages/console-frontend/src/routes/functions.tsx`:
- Around line 226-229: The dispatchInvocation call that sets the invocation
result is missing the HTTP status field defined on InvocationResult; update the
object passed to dispatchInvocation (inside the function that calls
invokeFunctionApi and currently uses dispatchInvocation({ type: 'SET_RESULT',
result: { success: result.success, duration, data: result.data, error:
result.error } })) to include status (e.g., status: result.status or the
response HTTP status returned by invokeFunctionApi) so the SET_RESULT payload
populates InvocationResult.status for better debugging.

In `@packages/console-frontend/src/routes/logs.tsx`:
- Around line 285-302: When filters change the current page can become invalid;
add an effect to reset pagination by calling setCurrentPage(1) whenever
searchQuery or activeLevelFilters change. Implement a useEffect that depends on
[searchQuery, activeLevelFilters] (or [searchQuery, activeLevelFilters, logs] if
you prefer) and inside call setCurrentPage(1); reference the existing
filteredLogs, currentPage and setCurrentPage state variables in the file to
locate where to add the effect.

In `@packages/console-frontend/src/routes/streams.tsx`:
- Around line 130-192: Introduce a named constant MAX_MESSAGES (e.g., const
MAX_MESSAGES = 1000) near the top of this module and replace the hard-coded 1000
in messagesReducer (the .slice(0, 1000) call) with MAX_MESSAGES; ensure any
future uses (if needed) reference MAX_MESSAGES and keep INITIAL_MESSAGES_STATE
unchanged, exporting MAX_MESSAGES if it needs to be reused elsewhere.

In `@packages/console-frontend/src/routes/traces.tsx`:
- Around line 284-317: The mousemove handler currently calls setPanelWidths on
every event causing excessive re-renders; throttle updates using
requestAnimationFrame by adding an RAF loop inside the useEffect: in onMouseMove
(used with isResizingRef and resizeStartRef) capture the desired width
calculation (using clampPanelWidth, PANEL_MIN_WIDTH, totalWidth, etc.) into a
local "next" value and schedule a single requestAnimationFrame to call
setPanelWidths(trace/ span) if one isn’t already pending; store the raf id in a
ref so you can cancel it on cleanup and when onMouseUp clears isResizingRef and
cancels any pending RAF to avoid orphaned frames.

In `@packages/console-frontend/src/routes/triggers.tsx`:
- Line 467: The filter callback for building queryEntries uses an unused named
variable `_` which is unnecessary; update the Object.entries call that produces
queryEntries to destructure only the used second element (e.g., use ([, v]) or
similar) so the unused key is not declared, i.e., change
Object.entries(queryParams).filter(([_, v]) => v) to use the
ignored-first-element pattern and keep the filter logic (reference: queryEntries
and queryParams).
- Around line 155-176: triggersUiReducer is missing a default branch so
unrecognized action types can return undefined at runtime; update the switch in
function triggersUiReducer to include a default case that returns the current
state (e.g., return state) to ensure runtime safety for unknown TriggersUiAction
values, and optionally include an exhaustive type guard (using a
never/assertUnreachable pattern) if you want compile-time checks in addition to
the default return.

Comment on lines +390 to +400
{isOpen && (
// biome-ignore lint/a11y/noStaticElementInteractions: click-away overlay with keyboard support
<div
role="presentation"
className="fixed inset-0 z-40"
onClick={() => setIsOpen(false)}
onKeyDown={(e) => {
if (e.key === 'Escape') setIsOpen(false)
}}
/>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Escape key handler won't work — the overlay is not focusable.

The onKeyDown handler on this <div> will never fire because the element has no tabIndex and cannot receive focus. Keyboard events only fire on the focused element (or bubble from descendants). The click-away works, but Escape key dismissal does not.

🔧 Suggested fix using a document-level listener

Replace the onKeyDown on the overlay with a useEffect:

+import { useCallback, useEffect, useMemo, useState } from 'react'
+  // Close dropdown on Escape key
+  useEffect(() => {
+    if (!isOpen) return
+    const handleKeyDown = (e: KeyboardEvent) => {
+      if (e.key === 'Escape') setIsOpen(false)
+    }
+    document.addEventListener('keydown', handleKeyDown)
+    return () => document.removeEventListener('keydown', handleKeyDown)
+  }, [isOpen])

Then simplify the overlay:

       {isOpen && (
-        // biome-ignore lint/a11y/noStaticElementInteractions: click-away overlay with keyboard support
         <div
           role="presentation"
           className="fixed inset-0 z-40"
           onClick={() => setIsOpen(false)}
-          onKeyDown={(e) => {
-            if (e.key === 'Escape') setIsOpen(false)
-          }}
         />
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx` around
lines 390 - 400, The overlay's onKeyDown won't fire because it isn't focusable;
modify TimeRangeFilter to add a document-level keydown listener via useEffect
that watches isOpen and, when isOpen is true, attaches a handler that calls
setIsOpen(false) on Escape and removes the listener on cleanup (or when isOpen
becomes false); remove the onKeyDown prop from the overlay <div> and keep the
click handler for click-away behavior while ensuring the useEffect references
setIsOpen and isOpen so the Escape key reliably closes the panel.

<div className="px-5 py-4 md:py-4.5 border-b border-border flex items-center justify-between">
<div className="flex items-center gap-3">
<IIILogo className="w-6 h-6 text-white" />
<img src="/iii-white1.1.svg" alt="iii logo" className="w-6 h-6" />
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check for base path / asset prefix configuration and existing asset URL conventions

echo "=== Finding bundler config files ==="
fd -a 'vite.config|webpack.config|next.config|package.json' | head -20

echo -e "\n=== Checking bundler configuration for base path ==="
rg -n "base:|publicPath|assetPrefix|homepage" \
  -g 'vite.config.*' -g 'webpack.config.*' -g 'next.config.*' -g 'package.json'

echo -e "\n=== Checking for BASE_URL or PUBLIC_URL usage ==="
rg -n "BASE_URL|PUBLIC_URL|assetPrefix" -g '*.{ts,tsx,js,jsx}' | head -20

echo -e "\n=== Finding all SVG asset paths ==="
rg -n "/iii-white1\.1\.svg|/iii-black1\.1\.svg" -g '*.{ts,tsx,js,jsx,html,css,scss}'

Repository: iii-hq/console

Length of output: 668


🏁 Script executed:

#!/bin/bash
echo "=== Vite configuration ==="
cat -n packages/console-frontend/vite.config.ts

echo -e "\n=== Check public directory structure ==="
find packages/console-frontend/public -type f 2>/dev/null | head -20

echo -e "\n=== Look for other img src usages ==="
rg -n '<img\s+src=' packages/console-frontend/src -g '*.{ts,tsx}' -A 1 | head -40

echo -e "\n=== Look for asset imports ==="
rg -n "import.*\.(svg|png|jpg|gif|webp)" packages/console-frontend/src -g '*.{ts,tsx}' | head -20

echo -e "\n=== Check how other SVG/images are referenced ==="
rg -n "import.*svg|from.*\.svg" packages/console-frontend/src -g '*.{ts,tsx}' | head -20

Repository: iii-hq/console

Length of output: 2912


🏁 Script executed:

#!/bin/bash
echo "=== Check Vite/package versions ==="
cat packages/console-frontend/package.json | jq '.devDependencies | {vite, react, "@vitejs/plugin-react"}'

echo -e "\n=== Verify BASE_URL is Vite-provided ==="
rg -n "import.meta.env" packages/console-frontend/src -g '*.{ts,tsx}' | head -10

Repository: iii-hq/console

Length of output: 441


Use import.meta.env.BASE_URL for asset paths to ensure compatibility with sub-path deployments.

Lines 123 and 266 hardcode /iii-white1.1.svg. While this works with the current configuration, if the app is deployed under a base path (e.g., by setting base in Vite config), these root-relative URLs would 404. Use import.meta.env.BASE_URL to prefix asset paths so they adapt to any deployment configuration.

🔧 Proposed adjustment
-          <img src="/iii-white1.1.svg" alt="iii logo" className="w-6 h-6" />
+          <img
+            src={`${import.meta.env.BASE_URL}iii-white1.1.svg`}
+            alt="iii logo"
+            className="w-6 h-6"
+          />
-          <img src="/iii-white1.1.svg" alt="iii logo" className="w-5 h-5" />
+          <img
+            src={`${import.meta.env.BASE_URL}iii-white1.1.svg`}
+            alt="iii logo"
+            className="w-5 h-5"
+          />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/layout/Sidebar.tsx` at line 123, The
img tag in Sidebar.tsx uses a hardcoded root-relative path "/iii-white1.1.svg"
(appears in the <img src=...> usage and again later) which will break under a
Vite base path; update both image src usages in the Sidebar component to prefix
the file with import.meta.env.BASE_URL (e.g., set src to
`${import.meta.env.BASE_URL}iii-white1.1.svg` or build the path via string
concatenation) so asset URLs respect the configured base path.

Comment on lines 55 to 57
<div
key={`${event.timestamp}-${index}`}
key={`${event.name}-${event.timestamp}`}
className={`rounded-lg border overflow-hidden ${
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat packages/console-frontend/src/components/traces/SpanLogsTab.tsx | head -100

Repository: iii-hq/console

Length of output: 3901


🏁 Script executed:

rg -n "interface VisualizationSpan|type VisualizationSpan" packages --type ts --type tsx

Repository: iii-hq/console

Length of output: 85


🏁 Script executed:

rg -n "interface VisualizationSpan|type VisualizationSpan" packages

Repository: iii-hq/console

Length of output: 226


🏁 Script executed:

cat packages/console-frontend/src/lib/traceTransform.ts

Repository: iii-hq/console

Length of output: 8300


🏁 Script executed:

fd -t f -name "*.ts" packages | xargs rg -n "interface StoredSpan|type StoredSpan" -A 20 | head -50

Repository: iii-hq/console

Length of output: 2038


🏁 Script executed:

rg -n "interface SpanEvent|type SpanEvent" packages

Repository: iii-hq/console

Length of output: 146


🏁 Script executed:

sed -n '8,20p' packages/console-frontend/src/api/observability/traces.ts

Repository: iii-hq/console

Length of output: 298


Potential React key collisions (name + timestamp may not be unique).

SpanEvent has no unique identifier field, so if multiple events share the same name and timestamp, React will see duplicate keys and may mis-render or drop updates. Add the index to guarantee uniqueness:

🔧 Recommended fix
-            key={`${event.name}-${event.timestamp}`}
+            key={`${event.name}-${event.timestamp}-${index}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/traces/SpanLogsTab.tsx` around lines
55 - 57, The list item key in SpanLogsTab.tsx currently uses
`${event.name}-${event.timestamp}` which can collide for SpanEvent objects
lacking a unique id; update the key generation in the map rendering where you
use `event` (the map callback inside SpanLogsTab) to include the iteration index
(or another unique identifier if available) so keys become unique (e.g., combine
event.name, event.timestamp, and the map index) to prevent React key collisions
and mis-renders.

Comment on lines +141 to +144
const [spanColWidth, setSpanColWidth] = useState(() => {
const saved = localStorage.getItem('iii-span-col-width')
return saved ? Number.parseInt(saved, 10) : 300
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add error handling for localStorage access.

localStorage.getItem can throw in private browsing mode or when storage is full. Wrap in try-catch to prevent runtime errors.

Suggested fix
   const [spanColWidth, setSpanColWidth] = useState(() => {
-    const saved = localStorage.getItem('iii-span-col-width')
-    return saved ? Number.parseInt(saved, 10) : 300
+    try {
+      const saved = localStorage.getItem('iii-span-col-width')
+      return saved ? Number.parseInt(saved, 10) : 300
+    } catch {
+      return 300
+    }
   })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [spanColWidth, setSpanColWidth] = useState(() => {
const saved = localStorage.getItem('iii-span-col-width')
return saved ? Number.parseInt(saved, 10) : 300
})
const [spanColWidth, setSpanColWidth] = useState(() => {
try {
const saved = localStorage.getItem('iii-span-col-width')
return saved ? Number.parseInt(saved, 10) : 300
} catch {
return 300
}
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/components/traces/WaterfallChart.tsx` around
lines 141 - 144, The localStorage access in the spanColWidth initialiser can
throw (e.g., private mode), so update the useState initializer in
WaterfallChart.tsx to wrap localStorage.getItem('iii-span-col-width') in a
try-catch, parse the value only if retrieved and valid (fallback to 300 on any
error or NaN), and similarly wrap any future localStorage.setItem calls for
spanColWidth in try-catch to avoid runtime errors; reference the spanColWidth
and setSpanColWidth state variables to locate the code to change.

Comment on lines +650 to 661
{hasError && (
<div className="bg-yellow/10 border border-yellow/30 rounded-lg p-3 flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-yellow flex-shrink-0" />
<div>
<p className="text-sm font-medium text-yellow">Connection Issue</p>
<p className="text-xs text-muted">
Unable to connect to the iii engine. Check that it's running on the expected host and
port.
</p>
</div>
</div>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Connection Issue card relies on unreachable code path.

Since the loader always redirects, this error handling UI will never be displayed to users. If error handling is needed for the /functions route instead, it should be implemented there.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/index.tsx` around lines 650 - 661, The
Connection Issue UI block controlled by hasError in routes/index.tsx is
unreachable because the loader always redirects; remove or relocate this error
handling and implement it in the functions route instead. Specifically, delete
or disable the unreachable JSX that renders the yellow "Connection Issue" card
(the hasError conditional) and add equivalent error handling and UI rendering in
the /functions route's component or loader error path (update the functions
route loader or component to set and render an error state). Ensure any state or
props names (hasError) are wired to the functions route logic so the UI becomes
reachable when that route detects the engine connection failure.

Comment on lines 456 to 458
const clearMessages = () => {
setMessages([])
setStats({
totalMessages: 0,
inbound: 0,
outbound: 0,
totalBytes: 0,
latency: stats.latency,
lastPingTime: stats.lastPingTime,
})
messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime })
setSelectedMessageId(null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether Pagination clamps currentPage to totalPages.
set -euo pipefail

files=$(fd -a 'pagination\.(t|j)sx?$')
if [ -z "$files" ]; then
  echo "No pagination component found."
  exit 1
fi

while IFS= read -r f; do
  echo "----- $f -----"
  sed -n '1,200p' "$f"
done <<< "$files"

Repository: iii-hq/console

Length of output: 3642


🏁 Script executed:

#!/bin/bash
# Check the streams.tsx file around clearMessages and currentPage usage
set -euo pipefail

# First, find and examine the relevant section of streams.tsx
FILE="packages/console-frontend/src/routes/streams.tsx"

if [ ! -f "$FILE" ]; then
  echo "File not found: $FILE"
  exit 1
fi

# Get total lines
TOTAL_LINES=$(wc -l < "$FILE")
echo "Total lines in $FILE: $TOTAL_LINES"
echo ""

# Show the clearMessages function and surrounding context
echo "----- Around clearMessages (lines 450-470) -----"
sed -n '450,470p' "$FILE"
echo ""

# Look for currentPage state declaration
echo "----- Searching for currentPage state -----"
rg -n 'currentPage|setCurrentPage' "$FILE" | head -20
echo ""

# Look for how messages are filtered/paginated
echo "----- Searching for pagination logic with currentPage -----"
rg -n -B2 -A2 'currentPage.*pageSize\|pageSize.*currentPage' "$FILE" | head -40

Repository: iii-hq/console

Length of output: 1178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/console-frontend/src/routes/streams.tsx"

# Show the pagination calculation logic
echo "----- Pagination calculation (lines 440-450) -----"
sed -n '440,450p' "$FILE"
echo ""

# Show more context around currentPage state and usage
echo "----- currentPage state and related logic (lines 220-230) -----"
sed -n '220,230p' "$FILE"
echo ""

# Look for the useMemo that uses filteredMessages and currentPage
echo "----- useMemo with currentPage (lines 438-455) -----"
sed -n '438,455p' "$FILE"
echo ""

# Find how paginatedMessages is used in rendering
echo "----- Search for paginatedMessages usage -----"
rg -n 'paginatedMessages' "$FILE"

Repository: iii-hq/console

Length of output: 2054


Reset pagination when clearing messages.

When the user clears messages from a later page (e.g., page 5), currentPage remains unchanged. Since the Pagination component does not clamp internally and uses filteredMessages.slice((currentPage - 1) * pageSize, ...), the list will be empty until enough new messages arrive to fill that page. Resetting to page 1 ensures the list displays correctly immediately after clearing.

Suggested fix
 const clearMessages = () => {
   messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime })
   setSelectedMessageId(null)
+  setCurrentPage(1)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const clearMessages = () => {
setMessages([])
setStats({
totalMessages: 0,
inbound: 0,
outbound: 0,
totalBytes: 0,
latency: stats.latency,
lastPingTime: stats.lastPingTime,
})
messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime })
setSelectedMessageId(null)
const clearMessages = () => {
messagesDispatch({ type: 'clear', latency: stats.latency, lastPingTime: stats.lastPingTime })
setSelectedMessageId(null)
setCurrentPage(1)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/console-frontend/src/routes/streams.tsx` around lines 456 - 458, The
clearMessages handler currently clears messages and resets selection but doesn't
reset pagination, causing an empty view if the user was on a later page; update
the clearMessages function (where messagesDispatch and setSelectedMessageId are
called) to also reset pagination by calling setCurrentPage(1) so the Pagination
component (which slices filteredMessages by currentPage and pageSize) shows page
1 immediately after clearing.

@andersonleal andersonleal merged commit efd4ccb into main Feb 20, 2026
1 check passed
@andersonleal andersonleal deleted the feat/trace-improvments branch February 20, 2026 19:30
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants