feat: architecture refactor - Rust binary with embedded frontend#1
feat: architecture refactor - Rust binary with embedded frontend#1andersonleal merged 1 commit intomainfrom
Conversation
fix: correct WebSocket port from 31112 to 3112 to match engine config Merge: Implement groups enumeration and fix API configuration bugs State Management: - Implement list_groups across all state adapters (Redis, KvStore, Bridge) - Update console handler to call state.list_groups with proper response formatting - Add comprehensive design docs with 9-step implementation plan API/UI Fixes: - Fix fetchStreams using wrong env var (VITE_WS_PORT → VITE_III_WS_PORT) - Centralize WS_PORT configuration to eliminate duplicated logic - Fix Sidebar health check hardcoded localhost, now uses dynamic config - Fix fetchTraces fallback (DEVTOOLS_API → MANAGEMENT_API) - Clean up unused imports and add clarifying TODO comments Includes end-to-end testing strategy, exact file paths, and code snippets in documentation.
📝 WalkthroughWalkthroughThe PR restructures the project from a single frontend package to a monorepo workspace with separate frontend (Vite/React) and Rust backend console packages, adds comprehensive API layer with React Query integration, removes the example application, and consolidates build tooling configuration. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User Browser
participant Console as Console Server<br/>(Rust/AXUM)
participant Engine as iii Engine
participant DB as Data Sources<br/>(Redis/etc)
User->>Console: HTTP Request (index.html)
Console->>Console: Embed config in HTML
Console->>User: SPA with env vars
User->>Console: Fetch /console/functions
Console->>Engine: bridge.invoke('functions')
Engine->>DB: Query functions
DB->>Engine: Return results
Engine->>Console: Response
Console->>User: JSON response
User->>Console: WebSocket subscribe
Console->>Engine: WebSocket bridge connect
Engine->>Engine: Stream updates
Engine->>Console: Message stream
Console->>User: Live updates
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/console-frontend/src/components/ui/pagination.tsx (1)
89-101:⚠️ Potential issue | 🟡 MinorNext/Last buttons not disabled when there are no pages.
When
totalPagesis0(empty dataset), the disabled checkcurrentPage === totalPagesevaluates tofalse(assumingcurrentPagedefaults to1), leaving these buttons enabled.Proposed fix
<Button variant="ghost" size="sm" onClick={() => onPageChange(currentPage + 1)} - disabled={currentPage === totalPages} + disabled={currentPage >= totalPages || totalPages === 0} className="h-6 md:h-7 w-6 md:w-7 p-0" title="Next page" ><Button variant="ghost" size="sm" onClick={() => onPageChange(totalPages)} - disabled={currentPage === totalPages} + disabled={currentPage >= totalPages || totalPages === 0} className="h-6 md:h-7 w-6 md:w-7 p-0" title="Last page" >
🤖 Fix all issues with AI agents
In `@packages/console-frontend/src/api/alerts/alerts.ts`:
- Around line 39-41: The fallback path returns res.json() which is inconsistent
with the primary path that uses unwrapResponse<AlertsResponse>(res); update the
fallback to parse using the same helper (call
unwrapResponse<AlertsResponse>(res)) or replace the whole fetch with the
centralized fetchWithFallback utility so both primary and fallback produce the
same wrapped AlertsResponse shape; ensure you reference the same MANAGEMENT_API
URL and return the result typed as AlertsResponse to match the primary branch.
In `@packages/console-frontend/src/api/config.ts`:
- Around line 38-44: DEVTOOLS_API, MANAGEMENT_API and STREAMS_WS currently
hardcode http/ws schemes which breaks under HTTPS; change their construction to
derive the scheme from the runtime page protocol (use window.location.protocol
for http/https and map to ws/wss for websockets) and allow an optional config
override (e.g., a passed-in protocol or env flag) so ENGINE_HOST, ENGINE_PORT
and WS_PORT are combined with a computed scheme instead of "http://" or "ws://";
update the constants DEVTOOLS_API, MANAGEMENT_API and STREAMS_WS to use that
computed protocol logic so they work correctly under both http and https.
In `@packages/console-frontend/src/components/filters/TimeRangeFilter.tsx`:
- Around line 209-233: The TimeRangeFilter component has three issues: avoid
nesting a <button> inside another by moving the clear control out of the main
toggle button into a sibling button (adjust the onClick to stopPropagation and
call handleClear), remove the unused defaultPreset prop from the
TimeRangeFilterProps type/props list and any destructuring, and harden
custom-range validation in handleApplyCustom by ensuring parseDateTimeLocal
returns valid numbers (guard against isNaN for both startTime and endTime and
that both inputs are present) before checking startTime >= endTime and calling
onChange; update the error/validation path to block applying when either value
is invalid.
In `@packages/console-frontend/src/components/traces/SpanLogsTab.tsx`:
- Around line 7-16: formatTimestamp is being passed nanosecond timestamps (e.g.,
event.timestamp) but constructs a Date expecting milliseconds, producing wrong
times; import toMs from `@/lib/traceTransform` and convert event.timestamp before
calling formatTimestamp (call formatTimestamp(toMs(event.timestamp))) or
alternatively change callers to pass toMs-converted values so formatTimestamp
receives milliseconds; reference the formatTimestamp function and the
event.timestamp usage in the SpanLogsTab rendering and add the toMs conversion
where the event timestamp is formatted.
In `@packages/console-frontend/src/components/traces/TraceMap.tsx`:
- Around line 69-82: The loop uses data.spans.find(...) causing O(n²) behavior;
fix by building a one-time lookup map (e.g., spanById) from span_id to span
before iterating, then replace data.spans.find((s) => s.span_id ===
span.parent_span_id) with spanById.get(span.parent_span_id); keep the rest of
the logic that computes fromService/toService and updates edgeMap
(callCount/totalDuration) unchanged, ensuring you reference the same symbols
(data.spans, span.parent_span_id, edgeMap) when making the replacement.
In `@packages/console-frontend/src/hooks/useTraceFilters.ts`:
- Around line 154-210: The getApiParams function currently mutates component
state by calling setValidationWarnings; extract the pure computation into a
useMemo that returns both computed TracesFilterParams and a warnings object
(compute the same params and warnings logic inside the memo), replace
getApiParams with a pure getter that only returns the memoized params, and add a
useEffect that watches the memoized warnings and calls
setValidationWarnings(warnings) (or clears them) so state updates happen outside
the getter; refer to getApiParams, setValidationWarnings, and introduce
useMemo/useEffect to perform this separation.
In `@packages/console-frontend/src/routes/handlers.tsx`:
- Around line 269-270: The API tester currently hard-codes fullUrl to
"http://localhost:3111", so update invokeFunction to call getConnectionInfo()
and build fullUrl from the returned engineHost and enginePort (not apiBase);
compose the base as `${engineHost}${enginePort ? ':' + enginePort : ''}` with
the appropriate protocol (e.g., http) and then append `${path}${queryString}`,
then use that fullUrl with the existing fetchOptions and fetch call (ensure
variables fullUrl, fetchOptions and invokeFunction are the ones updated).
In `@packages/console-frontend/src/routes/streams.tsx`:
- Around line 176-178: The subscriptionsRef Map is keyed only by streamName so
multiple group subscriptions for the same stream overwrite each other; change
all Map uses (initialization of subscriptionsRef, where entries are set,
retrieved and deleted—e.g., in the subscribe and unsubscribe logic and any
lookup code around lines ~319-378) to use a composite key
`${streamName}:${groupId}` so each (stream,group) pair has its own entry, and
update references to access subscriptionId via
subscriptionsRef.get(`${streamName}:${groupId}`) instead of
subscriptionsRef.get(streamName).
- Around line 198-311: The effect currently recreates the WebSocket when
isPaused changes, which causes subscriptions to be dropped; stop including
isPaused in the useEffect dependency array and instead introduce a ref (e.g.,
isPausedRef) that you update in a separate useEffect to reflect pause state;
keep the connection logic in connect/ wsRef and use isPausedRef inside
ws.onmessage/onopen handlers to gate message handling/metrics without
closing/reconnecting the socket, leaving addMessage, connect, wsRef, wsConnected
state and cleanup logic unchanged.
In `@packages/console-frontend/src/routes/traces.tsx`:
- Around line 99-168: The loadTraces function currently ignores TraceFilters;
update loadTraces to pass the current filter state into fetchTraces so backend
filtering works: use the filterState (from useTraceFilters) and any derived
values (e.g., activeFilterCount) when building the request payload/params and
call fetchTraces with those params instead of just { limit: 10000 }; modify the
fetch call inside loadTraces (the call to fetchTraces within loadTraces) to
include filterState fields (and keep limit) so fetchTraces receives the UI
filters and the backend can apply them.
- Around line 282-443: The trace list rendering ignores search, system toggle
and pagination: compute a derived array (e.g., const filteredTraces = useMemo(()
=> { /* filter traceGroups by searchQuery against group.traceId and
group.rootOperation, exclude system traces when showSystem is false (use
whatever flag/property identifies system traces on group) */ return result; },
[traceGroups, searchQuery, showSystem]);), then compute pagination (const
totalItems = filteredTraces.length; const totalPages = Math.ceil(totalItems /
filterState.pageSize); const pagedTraces =
filteredTraces.slice((filterState.page-1)*filterState.pageSize,
filterState.page*filterState.pageSize);) and replace traceGroups.map(...) with
pagedTraces.map(...), and pass totalItems/totalPages to <Pagination> (use
filterState.pageSize for pageSize). Also ensure selectedTraceId is cleared or
clamped if it’s no longer present in pagedTraces.
In `@packages/console-frontend/src/vite-env.d.ts`:
- Around line 3-7: The ImportMetaEnv declaration is missing the actual env vars
used (VITE_III_ENGINE_HOST, VITE_III_ENGINE_PORT, VITE_III_WS_PORT); update the
interface ImportMetaEnv to include readonly VITE_III_ENGINE_HOST, readonly
VITE_III_ENGINE_PORT, and readonly VITE_III_WS_PORT (as strings) so TypeScript
matches the usage in src/api/config.ts where those symbols are accessed and
documented in the README; remove or keep the existing VITE_ENGINE_* entries only
if they are actually used elsewhere to avoid stale declarations.
In `@packages/console-rust/src/bridge/functions.rs`:
- Around line 350-352: Remove the temporary debug println! call that prints
path_params; either delete the line `println!("DEBUG: Received input: {:?}",
path_params);` or replace it with a structured tracing call such as
`tracing::debug!(path_params = ?path_params, "Received input")` and ensure
`tracing` is in scope (use tracing::debug or add `use tracing::debug;`) so
logging is consistent and production-ready; reference symbols: `path_params`,
`input.get`, and the println! call in `functions.rs`.
- Around line 48-56: The handle_config function currently returns hardcoded
host/port values; replace these with the runtime configuration by reading the
shared config used by the bridge (or the injected config struct) instead of
literals: fetch engine_host, engine_port, ws_port, console_port from the
bridge’s runtime config (or environment/CLI-backed Config struct) and build the
JSON using those values before calling success_response; update the bridge
initialization to pass the Config into functions exposing handle_config (or have
handle_config access the singleton/shared Config) so the returned values reflect
actual env vars/CLI flags at runtime.
In `@packages/console-rust/src/main.rs`:
- Around line 79-81: The bridge URL currently hardcodes port 49134 (bridge_url /
iii_sdk::Bridge::new) while the CLI provides --ws-port for ServerConfig, causing
confusion; add a dedicated CLI option (e.g., --bridge-port) and corresponding
field in your args/ServerConfig (bridge_port), parse it from CLI, and use that
value when building bridge_url (replace the hardcoded 49134 with
args.bridge_port); alternatively if you intend the existing --ws-port to be the
bridge port, wire args.ws_port into bridge_url instead and add a clarifying
comment near bridge_url and the CLI flag to explain the distinction.
In `@packages/console-rust/src/server.rs`:
- Around line 121-125: The CorsLayer currently uses
.allow_origin(Any)/.allow_methods(Any)/.allow_headers(Any) which permits any
origin; update the CORS configuration (the CorsLayer instantiation where
CorsLayer::new() is used and methods allow_origin/allow_methods/allow_headers
are called) to restrict allowed origins to only "http://127.0.0.1" and
"http://localhost" (and their secure variants if needed) or wire it to a
configurable list from ServerConfig so production deployments can provide
expected client origins; ensure you remove or replace Any for allow_origin (and
keep Any for methods/headers only if intended) and document/validate that
binding to 0.0.0.0 is not sufficient to justify permissive CORS.
In `@README.md`:
- Around line 261-264: The README API table is wrong: replace the documented
`/_console/streams/...` endpoints and methods with the actual Rust routes from
packages/console-rust/src/bridge/triggers.rs — change `/_console/streams/groups`
POST to `/_console/states/groups` GET, change `/_console/streams/group` POST to
`/_console/states/group` POST, change `/_console/streams/:stream/group/:group`
POST to `/_console/states/:group/item` POST, and change
`/_console/streams/:stream/group/:group/:key` DELETE to
`/_console/states/:group/item/:key` DELETE; update the table rows in README.md
accordingly and ensure path parameter names match `:group` and `:key`.
🟡 Minor comments (24)
packages/console-frontend/src/components/ui/json-viewer.tsx-85-95 (1)
85-95:⚠️ Potential issue | 🟡 MinorPotential stack overflow with deeply nested or circular data.
The recursive
JsonValuecomponent has no protection against circular references. WhilemaxDepthlimits rendering depth, ifdatacontains circular references andmaxDepthis set high, or if someone passes a self-referencing object, this could cause issues. TheJSON.stringifyin the copy function would also throw on circular data.Consider adding a
WeakSetto track visited objects, or document that circular references are unsupported.packages/console-frontend/src/components/ui/json-viewer.tsx-19-19 (1)
19-19:⚠️ Potential issue | 🟡 MinorInvalid Tailwind class:
text-yellowmissing shade.The
text-yellowclass is incomplete. Tailwind color utilities require a shade suffix (e.g.,text-yellow-400). Without it, boolean values won't be styled correctly.Proposed fix
- boolean: 'text-yellow', + boolean: 'text-yellow-400',packages/console-frontend/src/components/ui/pagination.tsx-29-30 (1)
29-30:⚠️ Potential issue | 🟡 MinorEdge case: incorrect display when
totalItemsis 0.When there are no items,
startItemcomputes to1whileendItemis0, resulting in "1-0 of 0" which is confusing. Consider guarding against this:Proposed fix
- const startItem = (currentPage - 1) * pageSize + 1 - const endItem = Math.min(currentPage * pageSize, totalItems) + const startItem = totalItems === 0 ? 0 : (currentPage - 1) * pageSize + 1 + const endItem = Math.min(currentPage * pageSize, totalItems).gitignore-27-28 (1)
27-28:⚠️ Potential issue | 🟡 MinorThe
.env*pattern may ignore example environment files.The wildcard pattern
.env*will match.env.exampleor.env.templatefiles, which are often committed to the repository as safe templates (without secrets) to help developers set up their local environment.🔧 Proposed fix to preserve example files
# Environment -.env* +.env +.env.local +.env.*.local +!.env.examplepackages/console-frontend/src/components/layout/Sidebar.tsx-64-67 (1)
64-67:⚠️ Potential issue | 🟡 MinorMisleading comment and no-op effect.
The comment states "Close mobile menu on route change" but the empty dependency array
[]means this only runs once on mount. SinceisMobileMenuOpeninitializes tofalse, this effect does nothing useful.Either:
- Add
pathnameto the dependency array to actually close on route changes, or- Remove this effect entirely since the
onClickhandler on eachLink(line 125) already closes the menu.Option 1: Fix the effect to work as documented
// Close mobile menu on route change useEffect(() => { setIsMobileMenuOpen(false) - }, []) + }, [pathname])Option 2: Remove the redundant effect
- // Close mobile menu on route change - useEffect(() => { - setIsMobileMenuOpen(false) - }, []) -packages/console-frontend/README.md-158-162 (1)
158-162:⚠️ Potential issue | 🟡 Minor
PORTenvironment variable may not work with Vite.Vite doesn't use the
PORTenvironment variable by default. The server port is configured invite.config.ts. Unless custom handling is added, this suggestion won't work as documented.📝 Suggested documentation fix
2. Use a different port: ```bash - # Edit vite.config.ts and change server.port, or: - PORT=3114 pnpm dev + # Edit vite.config.ts and change server.port ``` </details> Or alternatively, add `PORT` handling in `vite.config.ts`: ```typescript server: { port: parseInt(process.env.PORT || '3113', 10), }lefthook.yml-5-5 (1)
5-5:⚠️ Potential issue | 🟡 MinorGlob pattern may miss files in subdirectories.
The glob
*.{ts,tsx,js,json}only matches files directly in thepackages/console-frontend/root directory. Most source files are typically insrc/and other subdirectories, which this pattern won't capture.🔧 Proposed fix to match files recursively
- glob: "*.{ts,tsx,js,json}" + glob: "**/*.{ts,tsx,js,json}"packages/console-rust/Cargo.toml-13-13 (1)
13-13:⚠️ Potential issue | 🟡 MinorFix formatting issue and comment misplacement on line 13.
Line 13 has a malformed comment merged with the dependency declaration, and the comment "# Async runtime" is incorrectly associated with
iii-sdkwhen it should describe thetokiodependency below it.🔧 Proposed fix
-iii-sdk = { path = "../../../iii-engine/packages/rust/iii" }# Async runtime +iii-sdk = { path = "../../../iii-engine/packages/rust/iii" } + +# Async runtime tokio = { version = "1", features = ["full"] }The
iii-sdkpath dependency pointing to../../../iii-engine/packages/rust/iiiwill break builds for anyone without that exact sibling repository structure. Consider publishingiii-sdkto a registry or using a git dependency for better portability, and ensure CI/CD and other developers have documented access to the expected path structure.README.md-386-392 (1)
386-392:⚠️ Potential issue | 🟡 MinorUpdate tech stack documentation and fix WebSocket port defaults in README.
The Tech Stack section (lines 386-392) still references Next.js 16 and SWR, but the project now uses Vite 7.3.1 and
@tanstack/react-query. Additionally, the README documents the WebSocket port default as 31112, butpackages/console-frontend/src/api/config.tsshows the actual default is 3112. Update both the tech stack list and all WebSocket port references in the README to match the current configuration.packages/console-frontend/src/components/filters/TimeRangeFilter.tsx-36-44 (1)
36-44:⚠️ Potential issue | 🟡 MinorAdd
defaultPresetto the component or remove it from the interface.The
defaultPresetprop is declared inTimeRangeFilterPropsbut is neither destructured in the function parameters nor used anywhere in the implementation. Either wire it in (e.g., applying the preset on mount whenvalueis undefined) or remove it from the interface to avoid a misleading API.Example: Apply default preset on mount
-import { useCallback, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' export function TimeRangeFilter({ value, onChange, onClear, className = '', showCustomOption = true, + defaultPreset, compactMode = false, }: TimeRangeFilterProps) { + useEffect(() => { + if (!value && defaultPreset) { + const { startTime, endTime } = getTimeRangeFromPreset(defaultPreset) + onChange?.({ startTime, endTime, preset: defaultPreset }) + } + }, [defaultPreset, value, onChange])packages/console-frontend/src/api/events/invocation.ts-8-12 (1)
8-12:⚠️ Potential issue | 🟡 MinorUse nullish coalescing instead of logical OR for input parameter.
input || {}converts falsy values like0,false, and''to{}. Sinceinputacceptsunknowntype, these are valid inputs that should be preserved. Useinput ?? {}to only default onundefinedornull.Proposed fix
- body: JSON.stringify({ function_path: functionPath, input: input || {} }), + body: JSON.stringify({ function_path: functionPath, input: input ?? {} }),packages/console-frontend/src/components/filters/TimeRangeFilter.tsx-145-152 (1)
145-152:⚠️ Potential issue | 🟡 MinorGuard against invalid datetime inputs.
When an input field is cleared,parseDateTimeLocalreturnsNaN. The current checkstartTime >= endTimedoes not catchNaNbecause comparisons withNaNalways returnfalse, allowing invalid values to be passed to theonChangehandler.Proposed fix (finite checks)
const startTime = parseDateTimeLocal(customStart) const endTime = parseDateTimeLocal(customEnd) - if (startTime >= endTime) { + if (!Number.isFinite(startTime) || !Number.isFinite(endTime) || startTime >= endTime) { // Invalid range - end must be after start return }packages/console-frontend/src/components/traces/ServiceBreakdown.tsx-61-64 (1)
61-64:⚠️ Potential issue | 🟡 MinorPotential division by zero when calculating percentages.
If
data.total_duration_msis 0 (e.g., for a trace with zero-duration spans or edge cases), this will produceInfinityorNaNpercentages, which could break the pie chart rendering.Proposed fix
const totalDuration = data.total_duration_ms for (const stats of statsMap.values()) { - stats.percentage = (stats.totalDuration / totalDuration) * 100 + stats.percentage = totalDuration > 0 ? (stats.totalDuration / totalDuration) * 100 : 0 }packages/console-frontend/src/api/observability/traces.ts-97-104 (1)
97-104:⚠️ Potential issue | 🟡 MinorSame inconsistency as logs.ts: fallback path doesn't use
unwrapResponse.For consistency with the primary DEVTOOLS path (line 91), the MANAGEMENT fallback should also use
unwrapResponse.Proposed fix
const res = await fetch(`${MANAGEMENT_API}/otel/traces`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) throw new Error('Failed to fetch traces') - return res.json() + return unwrapResponse<TracesResponse>(res) }packages/console-frontend/src/api/observability/logs.ts-99-106 (1)
99-106:⚠️ Potential issue | 🟡 MinorInconsistent response handling between primary and fallback API paths.
The primary DEVTOOLS path uses
unwrapResponse()(line 93) while the fallback MANAGEMENT path returnsres.json()directly (line 105). This could lead to different response shapes ifunwrapResponseperforms any transformation or unwrapping logic.Proposed fix for consistency
const res = await fetch(`${MANAGEMENT_API}/otel/logs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) if (!res.ok) throw new Error('Failed to fetch OTEL logs') - return res.json() + return unwrapResponse<OtelLogsResponse>(res) }packages/console-frontend/src/components/traces/SpanTagsTab.tsx-22-27 (1)
22-27:⚠️ Potential issue | 🟡 MinorUnhandled promise rejection on clipboard write.
navigator.clipboard.writeText()returns a Promise that isn't being awaited or caught. This could cause unhandled promise rejections in non-secure contexts (non-HTTPS) or when clipboard permissions are denied.Proposed fix with error handling
- const copyToClipboard = (key: string, value: unknown) => { + const copyToClipboard = async (key: string, value: unknown) => { const text = `${key}: ${JSON.stringify(value)}` - navigator.clipboard.writeText(text) - setCopiedKey(key) - setTimeout(() => setCopiedKey(null), 2000) + try { + await navigator.clipboard.writeText(text) + setCopiedKey(key) + setTimeout(() => setCopiedKey(null), 2000) + } catch { + // Clipboard API may fail in non-secure contexts + console.warn('Failed to copy to clipboard') + } }packages/console-frontend/src/components/ui/card.tsx-27-36 (1)
27-36:⚠️ Potential issue | 🟡 MinorFix CardTitle ref type to match the rendered element.
CardTitlerenders an<h3>element but the ref is typed asHTMLParagraphElement. The ref type should beHTMLHeadingElementto match the heading element being rendered.🔧 Suggested fix
-export const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> +export const CardTitle = React.forwardRef< + HTMLHeadingElement, + React.HTMLAttributes<HTMLHeadingElement>packages/console-frontend/src/api/observability/metrics.ts-132-165 (1)
132-165:⚠️ Potential issue | 🟡 MinorRemove redundant rollups retry or switch primary endpoint.
Both the try block and fallback call
MANAGEMENT_API/rollups, which duplicates the request and never attempts devtools.🛠️ Suggested fix (align with devtools-first pattern)
- const res = await fetch(`${MANAGEMENT_API}/rollups`, { + const res = await fetch(`${DEVTOOLS_API}/rollups`, {packages/console-frontend/src/components/traces/FlameGraph.tsx-264-271 (1)
264-271:⚠️ Potential issue | 🟡 MinorClamp horizontal pan to graph bounds.
Currently panning can move beyond the rendered graph and show empty space.
🛠️ Suggested fix
- } else { - setPanOffset((prev) => Math.max(0, prev + e.deltaY)) - } + } else { + const container = containerRef.current + const width = container?.clientWidth ?? 0 + const visibleWidth = Math.max(0, width - PADDING * 2) + const graphWidth = visibleWidth * zoomLevel + const maxPan = Math.max(0, graphWidth - visibleWidth) + setPanOffset((prev) => Math.min(maxPan, Math.max(0, prev + e.deltaY))) + }packages/console-frontend/src/components/traces/SpanBaggageTab.tsx-44-46 (1)
44-46:⚠️ Potential issue | 🟡 MinorJSON pretty-printing won't display correctly.
JSON.stringify(value, null, 2)adds newlines and indentation, but withoutwhitespace-preor<pre>styling, the formatting will collapse into a single line.♻️ Suggested fix
- <div className="text-sm text-gray-300 break-all font-mono"> + <div className="text-sm text-gray-300 break-all font-mono whitespace-pre-wrap"> {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)} </div>packages/console-frontend/src/routes/logs.tsx-193-223 (1)
193-223:⚠️ Potential issue | 🟡 MinorPagination reset doesn't react to filter changes.
The useEffect at line 220 has an empty dependency array, so it only runs once on mount. When users change filters (searchQuery, activeLevelFilters, timeRange, or selectedSeverity), the filtered results update but the current page isn't reset, potentially leaving users on an empty page.
Fix by adding filter dependencies:
✅ Suggested dependency fix
- useEffect(() => { - setCurrentPage(1) - }, []) + useEffect(() => { + setCurrentPage(1) + }, [searchQuery, activeLevelFilters, timeRange, selectedSeverity])packages/console-frontend/src/routes/index.tsx-156-165 (1)
156-165:⚠️ Potential issue | 🟡 MinorLive indicator can be true even when WS fails.
setStreamConnected(true)runs immediately afterconnect(), regardless of whether the WebSocket actually opens or later disconnects. This can show “Live” when the stream isn’t connected. Consider wiring connection callbacks fromcreateMetricsSubscriptionso state reflects real status.🔌 Example wiring (requires updating createMetricsSubscription)
- const subscription = createMetricsSubscription(queryClient) - subscription.connect() - setStreamConnected(true) + const subscription = createMetricsSubscription(queryClient, { + onOpen: () => setStreamConnected(true), + onClose: () => setStreamConnected(false), + onError: () => setStreamConnected(false), + }) + subscription.connect()packages/console-frontend/src/routes/config.tsx-137-140 (1)
137-140:⚠️ Potential issue | 🟡 MinorEndpoint refresh stops if one refetch fails.
Promise.allrejects on the first failure, socheckEndpoints()never runs when an adapter/status refetch fails. Since endpoint health is independent, usePromise.allSettled(or atry/finally) to ensure the refresh still happens.✅ Always run endpoint checks
const loadData = async () => { - await Promise.all([refetchAdapters(), refetchStatus()]) - checkEndpoints() + await Promise.allSettled([refetchAdapters(), refetchStatus()]) + await checkEndpoints() }packages/console-frontend/src/routes/states.tsx-97-151 (1)
97-151:⚠️ Potential issue | 🟡 MinorGroup counts can go stale after add/edit/delete.
Mutations refetch items, but group counts in the sidebar are based on
groupsData. After add/edit/delete, the group list can be out of sync until a manual refresh. Trigger a groups refetch after successful mutations.🔄 Keep group counts in sync
await setStateItem(selectedGroupId, newKey, value) setNewKey('') setNewValue('') setShowAddModal(false) refetchItems() + refetchGroups() @@ await deleteStateItem(selectedGroupId, item.key) refetchItems() + refetchGroups() @@ await setStateItem(selectedGroupId, item.key, value) setEditingItem(null) setEditValue('') refetchItems() + refetchGroups()
🧹 Nitpick comments (33)
packages/console-frontend/src/components/traces/ViewSwitcher.tsx (1)
1-42: Clean implementation with proper TypeScript typing.The component is well-structured with correct icon usage, proper event handling, and clean conditional styling. One optional enhancement for accessibility: consider adding
aria-pressedto indicate the active state to screen readers.♿ Optional accessibility enhancement
<button key={id} type="button" onClick={() => onViewChange(id)} + aria-pressed={isActive} className={`packages/console-frontend/src/components/ui/error-boundary.tsx (1)
28-46: Consider adding a reset mechanism for error recovery.The error boundary works correctly, but once an error is caught, users must remount the component to recover. Adding a reset callback would improve UX by allowing users to retry without navigating away.
♻️ Optional enhancement with reset support
interface Props { children: ReactNode fallback?: ReactNode + onReset?: () => void } export class ErrorBoundary extends Component<Props, State> { // ... existing code ... + resetErrorBoundary = () => { + this.props.onReset?.() + this.setState({ hasError: false, error: undefined }) + } render() { if (this.state.hasError) { if (this.props.fallback) { return this.props.fallback } return ( <div className="p-4 bg-red-950/20 border border-red-500/30 rounded-lg"> <div className="flex items-center gap-2 text-red-400 mb-2"> <AlertCircle className="w-4 h-4" /> <span className="text-sm font-medium">Something went wrong</span> </div> <p className="text-xs text-gray-400">{this.state.error?.message}</p> + <button + onClick={this.resetErrorBoundary} + className="mt-2 text-xs text-blue-400 hover:text-blue-300" + > + Try again + </button> </div> ) } // ... } }packages/console-frontend/src/components/ui/json-viewer.tsx (2)
71-80: Add accessibility attributes to collapsible buttons.The collapse/expand buttons lack
aria-expandedandaria-labelattributes, which are needed for screen reader users to understand the button's purpose and state.Proposed fix for array collapse button
<button onClick={() => setIsCollapsed(!isCollapsed)} className="inline-flex items-center hover:bg-dark-gray/50 rounded px-0.5 -ml-0.5" + aria-expanded={!isCollapsed} + aria-label={isCollapsed ? `Expand array with ${value.length} items` : 'Collapse array'} >Apply the same pattern to the object collapse button at lines 119-128.
164-169: Handle clipboard API errors and cleanup timeout on unmount.The
navigator.clipboard.writeText()call is async but errors aren't handled—clipboard access can fail (permissions denied, non-HTTPS context). Additionally, thesetTimeoutcould fire after component unmount.Proposed fix with error handling and cleanup
+import { useCallback, useEffect, useRef, useState } from 'react' ... export function JsonViewer({ data, collapsed = false, depth = 0, maxDepth = 5, className = '', }: JsonViewerProps) { const [copied, setCopied] = useState(false) + const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null) + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + } + }, []) const copyToClipboard = useCallback(() => { const text = typeof data === 'string' ? data : JSON.stringify(data, null, 2) - navigator.clipboard.writeText(text) - setCopied(true) - setTimeout(() => setCopied(false), 2000) + navigator.clipboard.writeText(text).then( + () => { + setCopied(true) + timeoutRef.current = setTimeout(() => setCopied(false), 2000) + }, + (err) => { + console.error('Failed to copy:', err) + } + ) }, [data]).gitignore (2)
11-15: Consider using wildcards for better maintainability.The build output patterns are hardcoded to specific package names. If packages are renamed or new packages are added, these patterns will require manual updates.
♻️ Proposed refactor for more generic patterns
# Build outputs -packages/console-frontend/dist/ -packages/console-frontend/dist-binary/ -packages/console-rust/target/ -packages/console-rust/assets/ +packages/*/dist/ +packages/*/dist-binary/ +packages/*/target/ +packages/*/assets/
30-35: The*.logpattern is very broad.Line 35 ignores all
.logfiles, which makes the more specific patterns on lines 31-34 redundant. While this is a common pattern, it may inadvertently ignore legitimate log files that should be committed (e.g.,CHANGELOG.log, documentation logs, or intentional fixture files).If this breadth is intentional, consider removing the now-redundant specific patterns for clarity.
packages/console-frontend/src/components/layout/Sidebar.tsx (2)
162-164: Hardcoded version and host in status footer.The footer displays
localhost:3111but the health check uses dynamicgetConnectionInfo(). For consistency, consider using the same dynamic configuration here. The versionv0.0.0should also ideally come from a build-time constant or config.Suggested improvement
+ const { devtoolsApi } = getConnectionInfo() + // Extract host from devtoolsApi URL or use a dedicated config value + // In the JSX: <div className="mt-3 text-[9px] text-muted/60 tracking-wide font-mono"> - v0.0.0 • localhost:3111 + v0.0.0 • {new URL(devtoolsApi).host} </div>Alternatively, expose version and engine host as separate config values.
207-214: Minor: Duplicatelg:translate-x-0class.Line 211 adds
lg:translate-x-0unconditionally, and line 212's conditional also includeslg:translate-x-0in both branches. The standalone line 211 is redundant.Suggested cleanup
<div className={clsx( 'w-56 h-screen bg-black border-r border-border flex flex-col fixed left-0 top-0 z-50 transition-transform duration-300 ease-in-out', - // Mobile: slide in/out - 'lg:translate-x-0', - isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0', + // Desktop: always visible, Mobile: slide in/out + isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full', + 'lg:translate-x-0', )} >packages/console-rust/Cargo.toml (1)
36-40: Release profile optimizes aggressively for size.The
opt-level = "z"setting prioritizes minimal binary size over runtime performance. If the console has any CPU-intensive operations (e.g., JSON processing, data transformation), consider using"s"instead for a better size/performance balance.packages/console-rust/src/bridge/functions.rs (1)
443-559: Repetitive bridge registration pattern.The registration block has significant repetition with the clone-and-register pattern. While functional, this could be simplified with a macro in the future. For now, the code is acceptable but consider adding a comment noting this as a candidate for DRY improvement.
packages/console-frontend/src/lib/otel-utils.ts (1)
12-18: Consider usingNumber.isFiniteinstead of globalisFinite.The biome-ignore comment notes the global
isFiniteis safe for number types. However,Number.isFiniteis the modern approach that doesn't perform type coercion and would eliminate the need for the lint suppression.♻️ Proposed fix
export function nanoToMs(nanos: number): number { - // biome-ignore lint/suspicious/noGlobalIsFinite: isFinite is safe for number type - if (!isFinite(nanos)) { + if (!Number.isFinite(nanos)) { return 0 } return Math.floor(nanos / 1_000_000) }packages/console-frontend/src/components/traces/AttributesFilter.tsx (3)
1-1:'use client'directive may be unnecessary.This directive is specific to React Server Components (Next.js App Router). Since the PR migrates from Next.js to Vite, this directive has no effect and can be removed.
♻️ Proposed fix
-'use client' - import { Plus, X } from 'lucide-react'
12-12:editingIndexstate appears unused.The
editingIndexstate is set on focus/blur events but never used for any visual feedback or behavior. Either remove this dead state or implement the intended functionality (e.g., highlighting the active row).♻️ Proposed fix - remove unused state
export function AttributesFilter({ value, onChange }: AttributesFilterProps) { - const [editingIndex, setEditingIndex] = useState<number | null>(null) - const handleAdd = () => { onChange([...value, ['', '']]) - setEditingIndex(value.length) } const handleRemove = (index: number) => { const newAttrs = value.filter((_, i) => i !== index) onChange(newAttrs) - if (editingIndex === index) { - setEditingIndex(null) - } }And remove the
onFocus/onBlurhandlers from the inputs.
61-69: Add accessibility labels to inputs.The inputs rely solely on placeholder text for identification. Screen readers may not announce placeholders consistently. Consider adding
aria-labelattributes.♻️ Proposed enhancement
<input type="text" placeholder="Key (e.g., http.method)" + aria-label={`Attribute ${index + 1} key`} value={key}packages/console-frontend/src/api/utils.ts (2)
9-21: Consider handling JSON parse errors gracefully.If the response body is not valid JSON,
res.json()will throw, and the error message won't indicate it was a parse failure. This could make debugging harder when the server returns malformed responses.Proposed defensive handling
async function unwrapResponse<T>(res: Response): Promise<T> { - const data = await res.json() + let data: unknown + try { + data = await res.json() + } catch { + throw new Error(`Failed to parse JSON response from ${res.url}`) + } if (data && typeof data === 'object' && 'status_code' in data && 'body' in data) {
23-40: Fallback silently swallows the primary error.When the DEVTOOLS_API request fails, the original error is discarded. This can make it difficult to diagnose why the primary endpoint failed. Consider logging or including context about the fallback.
Optional: Add debug context
- } catch { - // Fall through to management API + } catch (primaryError) { + console.debug(`[fetchWithFallback] Primary API failed, falling back:`, primaryError) }packages/console-frontend/src/api/system/adapters.ts (1)
15-19: Consider usingfetchWithFallbackfor consistency.Other API modules (e.g.,
alerts.ts) implement fallback to the management API. If adapters should also have a fallback, consider using the centralizedfetchWithFallbackutility fromutils.tsfor consistency.If adapters are intentionally devtools-only, this is fine as-is.
packages/console-frontend/src/api/state/streams.ts (2)
26-26: Remove debug logging from production code.This
console.logstatement will appear in production builds. Consider removing it or gating behind a debug flag.Remove debug log
- console.log('[Streams API] Starting fetch from:', `${DEVTOOLS_API}/streams/list`)
42-50: Inconsistent error handling compared to other API modules.Other modules (e.g.,
adapters.ts,alerts.ts) throw errors on failure, while this module silently returns a fallback. This inconsistency can make error handling unpredictable for consumers.If the fallback is intentional (e.g., streams are optional), consider documenting this behavior or making it explicit in the function name/return type.
packages/console-frontend/src/api/websocket.ts (4)
14-14: Connection info is captured once at creation time.
getConnectionInfo()is called once when the subscription is created. If the connection configuration changes (e.g., port reconfiguration), the WebSocket won't use the updated URL until the subscription is recreated.Consider calling
getConnectionInfo()insideconnect()if dynamic configuration is needed.Also applies to: 129-129
77-86: Consider exponential backoff for reconnection.Fixed 3-second reconnect intervals may cause thundering herd issues if many clients reconnect simultaneously after a server restart. Exponential backoff with jitter would be more resilient.
Example exponential backoff pattern
let reconnectAttempts = 0 const MAX_BACKOFF = 30000 function getBackoffDelay() { const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), MAX_BACKOFF) const jitter = Math.random() * 1000 reconnectAttempts++ return delay + jitter } // Reset on successful connection ws.onopen = () => { reconnectAttempts = 0 // ... } // Use in reconnect reconnectTimer = setTimeout(connect, getBackoffDelay())
190-203: Mutable subscriptions array could have race conditions.
currentSubscriptionsis mutated directly insubscribe/unsubscribe. If these are called rapidly or from different async contexts, the array state could become inconsistent. Consider using a Set for O(1) lookups and cleaner add/remove semantics.Using Set for subscriptions
- let currentSubscriptions: string[] = [] + const currentSubscriptions = new Set<string>() // In subscribe: - if (!currentSubscriptions.includes(stream)) { - currentSubscriptions.push(stream) - } + currentSubscriptions.add(stream) // In unsubscribe: - currentSubscriptions = currentSubscriptions.filter((s) => s !== stream) + currentSubscriptions.delete(stream) // In connect's onopen: - currentSubscriptions.forEach((stream) => { + currentSubscriptions.forEach((stream) => {
167-174: Overwriting cache clears any existing messages.
connect()initializes the cache to an empty array (line 172), which will discard any previously captured messages ifconnectis called again (e.g., after disconnect/reconnect). If preserving history across reconnects is desired, consider making this behavior configurable.packages/console-frontend/src/hooks/useTraceFilters.ts (1)
68-129: PreferuseReffor debounce timers.
Using state for timers triggers extra renders and can leave stale timers; refs keep this logic stable.Proposed refactor (refs for timers)
-import { useCallback, useEffect, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' ... - const [serviceNameTimer, setServiceNameTimer] = useState<NodeJS.Timeout | null>(null) - const [operationNameTimer, setOperationNameTimer] = useState<NodeJS.Timeout | null>(null) + const serviceNameTimer = useRef<NodeJS.Timeout | null>(null) + const operationNameTimer = useRef<NodeJS.Timeout | null>(null) ... - useEffect(() => { - return () => { - if (serviceNameTimer) clearTimeout(serviceNameTimer) - if (operationNameTimer) clearTimeout(operationNameTimer) - } - }, [serviceNameTimer, operationNameTimer]) + useEffect(() => { + return () => { + if (serviceNameTimer.current) clearTimeout(serviceNameTimer.current) + if (operationNameTimer.current) clearTimeout(operationNameTimer.current) + } + }, []) ... - if (serviceNameTimer) clearTimeout(serviceNameTimer) - const timer = setTimeout(() => { + if (serviceNameTimer.current) clearTimeout(serviceNameTimer.current) + const timer = setTimeout(() => { setDebouncedServiceName(value as string | undefined) }, 300) - setServiceNameTimer(timer) + serviceNameTimer.current = timer ... - if (operationNameTimer) clearTimeout(operationNameTimer) - const timer = setTimeout(() => { + if (operationNameTimer.current) clearTimeout(operationNameTimer.current) + const timer = setTimeout(() => { setDebouncedOperationName(value as string | undefined) }, 300) - setOperationNameTimer(timer) + operationNameTimer.current = timerpackages/console-frontend/src/components/traces/TraceFilters.tsx (1)
21-27: Reuse shared time-range presets to avoid drift.
Consider sourcing values fromTIME_RANGE_PRESETS(orTimeRangeFilter) so presets stay consistent across the app.Example (reuse shared presets)
+import { TIME_RANGE_PRESETS } from '@/lib/timeRangeUtils' ... -const timeRangePresets = [ - { label: 'Last 15m', value: 15 * 60 * 1000 }, - { label: 'Last 1h', value: 60 * 60 * 1000 }, - { label: 'Last 6h', value: 6 * 60 * 60 * 1000 }, - { label: 'Last 24h', value: 24 * 60 * 60 * 1000 }, - { label: 'Last 7d', value: 7 * 24 * 60 * 60 * 1000 }, -] +const timeRangePresets = [ + { label: 'Last 15m', value: TIME_RANGE_PRESETS.LAST_15_MINUTES }, + { label: 'Last 1h', value: TIME_RANGE_PRESETS.LAST_1_HOUR }, + { label: 'Last 6h', value: TIME_RANGE_PRESETS.LAST_6_HOURS }, + { label: 'Last 24h', value: TIME_RANGE_PRESETS.LAST_24_HOURS }, + { label: 'Last 7d', value: TIME_RANGE_PRESETS.LAST_7_DAYS }, +]package.json (1)
27-29: Consider movingclass-variance-authorityto the frontend package.The
class-variance-authoritylibrary is typically a UI utility used directly by frontend components. Having it in the root workspacedependenciesrather than inpackages/console-frontend/package.jsonis unusual and may cause confusion about where the dependency is actually consumed.packages/console-frontend/src/api/observability/logs.ts (1)
119-126: Response body not consumed before returning success.When the fallback path succeeds, the response body is never read. While this works, not consuming the response body can interfere with HTTP connection reuse in some environments. Additionally, this differs from the primary path which calls
unwrapResponse.Proposed fix
const res = await fetch(`${MANAGEMENT_API}/otel/logs/clear`, { method: 'POST', }) if (!res.ok) throw new Error('Failed to clear OTEL logs') + await res.json().catch(() => {}) // Consume response body return { success: true } }packages/console-frontend/src/lib/traceTransform.ts (1)
164-169: Performance: O(n) lookups inside sort comparator leads to O(n² log n) complexity.The sort comparator performs
find()operations (O(n) each) on every comparison. For traces with many spans, this becomes expensive. Consider building a lookup map before sorting.Proposed fix using a pre-built map
+ // Build lookup map for efficient sorting + const spanStartTimes = new Map( + traceSpans.map((s) => [s.span_id, toMs(s.start_time_unix_nano)]) + ) + // Sort by start time, then by depth visualSpans.sort((a, b) => { - const aStart = toMs(traceSpans.find((s) => s.span_id === a.span_id)?.start_time_unix_nano ?? 0) - const bStart = toMs(traceSpans.find((s) => s.span_id === b.span_id)?.start_time_unix_nano ?? 0) + const aStart = spanStartTimes.get(a.span_id) ?? 0 + const bStart = spanStartTimes.get(b.span_id) ?? 0 if (aStart !== bStart) return aStart - bStart return a.depth - b.depth })packages/console-frontend/src/components/traces/TraceHeader.tsx (1)
9-13: DuplicateformatDurationfunction.This same helper exists in
ServiceBreakdown.tsx. Consider extracting to a shared utility (e.g.,@/lib/formatters.ts) to maintain consistency and reduce duplication.packages/console-frontend/src/components/traces/WaterfallChart.tsx (1)
16-20: Consider centralizing duration formatting.Waterfall/FlameGraph/TraceMap each define a local formatter; extracting a shared helper would reduce drift.
packages/console-rust/src/bridge/triggers.rs (1)
71-78: Consider reducing logging verbosity for trigger registration.Logging at
infolevel for each of the 20+ trigger registrations will produce noisy startup output. Thedebuglog at Line 71 is appropriate for detailed tracing, but theinfolog for every successful registration is excessive.Consider logging a single summary at the end instead:
♻️ Suggested refactor
for (function_path, api_path, method) in triggers { let config = json!({ "api_path": api_path, "http_method": method }); debug!("Registering API trigger: {} -> {}", api_path, function_path); bridge.register_trigger("api", function_path, config)?; - - info!( - "Successfully registered API trigger: {} -> {}", - api_path, function_path - ); } + info!("Successfully registered {} API triggers", triggers.len()); + Ok(())packages/console-rust/src/main.rs (1)
107-114: Server errors are silently swallowed on shutdown.When
shutdown_signal()fires, the server task is cancelled without checking if it completed with an error. If the server fails immediately after starting, the error would be lost.♻️ Consider logging server errors during shutdown
tokio::select! { - result = server => result, + result = server => { + if let Err(ref e) = result { + tracing::error!("Server error: {}", e); + } + result + }, _ = shutdown_signal() => { tracing::info!("Shutdown signal received, cleaning up..."); bridge.disconnect(); Ok(()) } }packages/console-frontend/src/components/traces/SpanInfoTab.tsx (1)
24-36: Consider extracting status configuration to reduce duplication.The status color and label mappings repeat the same keys. This could be consolidated for maintainability.
♻️ Optional consolidation
const STATUS_CONFIG = { ok: { color: 'bg-green-500', label: 'OK' }, error: { color: 'bg-red-500', label: 'ERROR' }, unset: { color: 'bg-gray-500', label: 'UNSET' }, } as const export function SpanInfoTab({ span }: SpanInfoTabProps) { const service = span.service_name || span.name.split('.')[0] const { color: statusColor, label: statusLabel } = STATUS_CONFIG[span.status] // ... }
| const res = await fetch(`${MANAGEMENT_API}/alerts`) | ||
| if (!res.ok) throw new Error('Failed to fetch alerts') | ||
| return res.json() |
There was a problem hiding this comment.
Inconsistent response handling between primary and fallback paths.
The primary path uses unwrapResponse<AlertsResponse>(res) (line 33), but the fallback path uses res.json() directly (line 41). If the management API also returns wrapped responses, this will return the wrong shape and could cause runtime errors.
Proposed fix: use unwrapResponse consistently
const res = await fetch(`${MANAGEMENT_API}/alerts`)
if (!res.ok) throw new Error('Failed to fetch alerts')
- return res.json()
+ return unwrapResponse<AlertsResponse>(res)Alternatively, if the management API intentionally returns unwrapped responses, consider using the centralized fetchWithFallback utility which handles this consistently.
📝 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.
| const res = await fetch(`${MANAGEMENT_API}/alerts`) | |
| if (!res.ok) throw new Error('Failed to fetch alerts') | |
| return res.json() | |
| const res = await fetch(`${MANAGEMENT_API}/alerts`) | |
| if (!res.ok) throw new Error('Failed to fetch alerts') | |
| return unwrapResponse<AlertsResponse>(res) |
🤖 Prompt for AI Agents
In `@packages/console-frontend/src/api/alerts/alerts.ts` around lines 39 - 41, The
fallback path returns res.json() which is inconsistent with the primary path
that uses unwrapResponse<AlertsResponse>(res); update the fallback to parse
using the same helper (call unwrapResponse<AlertsResponse>(res)) or replace the
whole fetch with the centralized fetchWithFallback utility so both primary and
fallback produce the same wrapped AlertsResponse shape; ensure you reference the
same MANAGEMENT_API URL and return the result typed as AlertsResponse to match
the primary branch.
| export const DEVTOOLS_API = `http://${ENGINE_HOST}:${ENGINE_PORT}/_console` | ||
| // TODO: Differentiate MANAGEMENT_API from DEVTOOLS_API when the engine supports separate endpoints. | ||
| // Currently both point to the same endpoint, but they represent different concerns: | ||
| // - DEVTOOLS_API: Primary endpoint for development tools and diagnostics | ||
| // - MANAGEMENT_API: Fallback endpoint for management operations (used when DEVTOOLS_API fails) | ||
| export const MANAGEMENT_API = `http://${ENGINE_HOST}:${ENGINE_PORT}/_console` | ||
| export const STREAMS_WS = `ws://${ENGINE_HOST}:${WS_PORT}` |
There was a problem hiding this comment.
Avoid fixed http/ws schemes to prevent mixed‑content failures.
If the console is served over HTTPS, these URLs will be blocked. Consider deriving schemes from window.location.protocol or allowing protocol overrides in config.
🔧 Suggested fix
-export const DEVTOOLS_API = `http://${ENGINE_HOST}:${ENGINE_PORT}/_console`
+const HTTP_SCHEME =
+ typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'https' : 'http'
+const WS_SCHEME = HTTP_SCHEME === 'https' ? 'wss' : 'ws'
+export const DEVTOOLS_API = `${HTTP_SCHEME}://${ENGINE_HOST}:${ENGINE_PORT}/_console`
...
-export const STREAMS_WS = `ws://${ENGINE_HOST}:${WS_PORT}`
+export const STREAMS_WS = `${WS_SCHEME}://${ENGINE_HOST}:${WS_PORT}`📝 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.
| export const DEVTOOLS_API = `http://${ENGINE_HOST}:${ENGINE_PORT}/_console` | |
| // TODO: Differentiate MANAGEMENT_API from DEVTOOLS_API when the engine supports separate endpoints. | |
| // Currently both point to the same endpoint, but they represent different concerns: | |
| // - DEVTOOLS_API: Primary endpoint for development tools and diagnostics | |
| // - MANAGEMENT_API: Fallback endpoint for management operations (used when DEVTOOLS_API fails) | |
| export const MANAGEMENT_API = `http://${ENGINE_HOST}:${ENGINE_PORT}/_console` | |
| export const STREAMS_WS = `ws://${ENGINE_HOST}:${WS_PORT}` | |
| const HTTP_SCHEME = | |
| typeof window !== 'undefined' && window.location.protocol === 'https:' ? 'https' : 'http' | |
| const WS_SCHEME = HTTP_SCHEME === 'https' ? 'wss' : 'ws' | |
| export const DEVTOOLS_API = `${HTTP_SCHEME}://${ENGINE_HOST}:${ENGINE_PORT}/_console` | |
| // TODO: Differentiate MANAGEMENT_API from DEVTOOLS_API when the engine supports separate endpoints. | |
| // Currently both point to the same endpoint, but they represent different concerns: | |
| // - DEVTOOLS_API: Primary endpoint for development tools and diagnostics | |
| // - MANAGEMENT_API: Fallback endpoint for management operations (used when DEVTOOLS_API fails) | |
| export const MANAGEMENT_API = `http://${ENGINE_HOST}:${ENGINE_PORT}/_console` | |
| export const STREAMS_WS = `${WS_SCHEME}://${ENGINE_HOST}:${WS_PORT}` |
🤖 Prompt for AI Agents
In `@packages/console-frontend/src/api/config.ts` around lines 38 - 44,
DEVTOOLS_API, MANAGEMENT_API and STREAMS_WS currently hardcode http/ws schemes
which breaks under HTTPS; change their construction to derive the scheme from
the runtime page protocol (use window.location.protocol for http/https and map
to ws/wss for websockets) and allow an optional config override (e.g., a
passed-in protocol or env flag) so ENGINE_HOST, ENGINE_PORT and WS_PORT are
combined with a computed scheme instead of "http://" or "ws://"; update the
constants DEVTOOLS_API, MANAGEMENT_API and STREAMS_WS to use that computed
protocol logic so they work correctly under both http and https.
| function formatTimestamp(timestamp: number): string { | ||
| const date = new Date(timestamp) | ||
| return date.toLocaleTimeString('en-US', { | ||
| hour12: false, | ||
| hour: '2-digit', | ||
| minute: '2-digit', | ||
| second: '2-digit', | ||
| fractionalSecondDigits: 3, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check the file structure to understand the project
fd -t f "\.ts$|\.tsx$" packages/console-frontend/src --max-results 20Repository: MotiaDev/iii-console
Length of output: 1048
🏁 Script executed:
# Find StoredSpan type definition
rg -n "StoredSpan" packages/console-frontend/src --type ts -A 3Repository: MotiaDev/iii-console
Length of output: 3617
🏁 Script executed:
# Check for otel-utils file
fd -n "otel-utils" packages/console-frontend/srcRepository: MotiaDev/iii-console
Length of output: 294
🏁 Script executed:
# Look for timestamp handling utilities
rg -n "nanoToMs|nanosecond|timestamp" packages/console-frontend/src --type ts | head -30Repository: MotiaDev/iii-console
Length of output: 3327
🏁 Script executed:
# Get the StoredSpan interface definition
rg -A 20 "export interface StoredSpan" packages/console-frontend/src/api/observability/traces.tsRepository: MotiaDev/iii-console
Length of output: 515
🏁 Script executed:
# Check SpanLogsTab.tsx to see how it uses timestamps
cat -n packages/console-frontend/src/components/traces/SpanLogsTab.tsxRepository: MotiaDev/iii-console
Length of output: 2677
🏁 Script executed:
# Check if toMs is used in SpanLogsTab
rg "toMs\|nanoToMs" packages/console-frontend/src/components/traces/SpanLogsTab.tsxRepository: MotiaDev/iii-console
Length of output: 46
🏁 Script executed:
# Search for SpanEvent interface
rg -A 10 "export interface SpanEvent" packages/console-frontend/srcRepository: MotiaDev/iii-console
Length of output: 911
🏁 Script executed:
# Check the api file for SpanEvent
cat packages/console-frontend/src/api/observability/traces.tsRepository: MotiaDev/iii-console
Length of output: 3855
Use toMs() to convert nanosecond event timestamps before formatting.
The event.timestamp is in nanoseconds (following OpenTelemetry format like start_time_unix_nano), but formatTimestamp() passes it directly to the Date constructor, which expects milliseconds. This will display incorrect times. Import toMs from @/lib/traceTransform and convert the timestamp:
{formatTimestamp(toMs(event.timestamp))}🤖 Prompt for AI Agents
In `@packages/console-frontend/src/components/traces/SpanLogsTab.tsx` around lines
7 - 16, formatTimestamp is being passed nanosecond timestamps (e.g.,
event.timestamp) but constructs a Date expecting milliseconds, producing wrong
times; import toMs from `@/lib/traceTransform` and convert event.timestamp before
calling formatTimestamp (call formatTimestamp(toMs(event.timestamp))) or
alternatively change callers to pass toMs-converted values so formatTimestamp
receives milliseconds; reference the formatTimestamp function and the
event.timestamp usage in the SpanLogsTab rendering and add the toMs conversion
where the event timestamp is formatted.
| data.spans.forEach((span) => { | ||
| if (span.parent_span_id) { | ||
| const parentSpan = data.spans.find((s) => s.span_id === span.parent_span_id) | ||
| if (parentSpan) { | ||
| const fromService = parentSpan.service_name || parentSpan.name.split('.')[0] || 'unknown' | ||
| const toService = span.service_name || span.name.split('.')[0] || 'unknown' | ||
|
|
||
| if (fromService !== toService) { | ||
| const edgeKey = `${fromService}->${toService}` | ||
| const existing = edgeMap.get(edgeKey) || { callCount: 0, totalDuration: 0 } | ||
| edgeMap.set(edgeKey, { | ||
| callCount: existing.callCount + 1, | ||
| totalDuration: existing.totalDuration + span.duration_ms, | ||
| }) |
There was a problem hiding this comment.
Avoid O(n²) parent lookup when building edges.
data.spans.find inside the loop scales poorly for large traces. Build a lookup map once and fetch parents in O(1).
⚡ Suggested fix
- // Second pass: build edges based on parent-child relationships
- data.spans.forEach((span) => {
- if (span.parent_span_id) {
- const parentSpan = data.spans.find((s) => s.span_id === span.parent_span_id)
+ const spanById = new Map(data.spans.map((s) => [s.span_id, s]))
+ // Second pass: build edges based on parent-child relationships
+ data.spans.forEach((span) => {
+ if (span.parent_span_id) {
+ const parentSpan = spanById.get(span.parent_span_id)🤖 Prompt for AI Agents
In `@packages/console-frontend/src/components/traces/TraceMap.tsx` around lines 69
- 82, The loop uses data.spans.find(...) causing O(n²) behavior; fix by building
a one-time lookup map (e.g., spanById) from span_id to span before iterating,
then replace data.spans.find((s) => s.span_id === span.parent_span_id) with
spanById.get(span.parent_span_id); keep the rest of the logic that computes
fromService/toService and updates edgeMap (callCount/totalDuration) unchanged,
ensuring you reference the same symbols (data.spans, span.parent_span_id,
edgeMap) when making the replacement.
| fn handle_config() -> Value { | ||
| success_response(json!({ | ||
| "engine_host": "localhost", | ||
| "engine_port": 3111, | ||
| "ws_port": 3112, | ||
| "console_port": 3113, | ||
| "version": env!("CARGO_PKG_VERSION") | ||
| })) | ||
| } |
There was a problem hiding this comment.
Hardcoded configuration values defeat dynamic configuration.
The handle_config function returns hardcoded host/port values, but the PR description mentions CLI flags and env vars replacing config files. This handler should read from the actual runtime configuration.
🔧 Suggested approach
Pass configuration through the bridge or use a shared config struct:
-fn handle_config() -> Value {
+fn handle_config(config: &AppConfig) -> Value {
success_response(json!({
- "engine_host": "localhost",
- "engine_port": 3111,
- "ws_port": 3112,
- "console_port": 3113,
+ "engine_host": config.engine_host,
+ "engine_port": config.engine_port,
+ "ws_port": config.ws_port,
+ "console_port": config.console_port,
"version": env!("CARGO_PKG_VERSION")
}))
}🤖 Prompt for AI Agents
In `@packages/console-rust/src/bridge/functions.rs` around lines 48 - 56, The
handle_config function currently returns hardcoded host/port values; replace
these with the runtime configuration by reading the shared config used by the
bridge (or the injected config struct) instead of literals: fetch engine_host,
engine_port, ws_port, console_port from the bridge’s runtime config (or
environment/CLI-backed Config struct) and build the JSON using those values
before calling success_response; update the bridge initialization to pass the
Config into functions exposing handle_config (or have handle_config access the
singleton/shared Config) so the returned values reflect actual env vars/CLI
flags at runtime.
| let path_params = input.get("path_params"); | ||
|
|
||
| println!("DEBUG: Received input: {:?}", path_params); |
There was a problem hiding this comment.
Remove debug println! statement.
This debug logging should be removed or replaced with structured tracing before release.
🔧 Proposed fix
- println!("DEBUG: Received input: {:?}", path_params);
+ tracing::debug!(?path_params, "Received state item delete request");📝 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.
| let path_params = input.get("path_params"); | |
| println!("DEBUG: Received input: {:?}", path_params); | |
| let path_params = input.get("path_params"); | |
| tracing::debug!(?path_params, "Received state item delete request"); |
🤖 Prompt for AI Agents
In `@packages/console-rust/src/bridge/functions.rs` around lines 350 - 352, Remove
the temporary debug println! call that prints path_params; either delete the
line `println!("DEBUG: Received input: {:?}", path_params);` or replace it with
a structured tracing call such as `tracing::debug!(path_params = ?path_params,
"Received input")` and ensure `tracing` is in scope (use tracing::debug or add
`use tracing::debug;`) so logging is consistent and production-ready; reference
symbols: `path_params`, `input.get`, and the println! call in `functions.rs`.
| // Initialize bridge connection to iii engine | ||
| let bridge_url = format!("ws://{}:49134", args.engine_host); | ||
| let bridge = iii_sdk::Bridge::new(&bridge_url); |
There was a problem hiding this comment.
Hardcoded WebSocket port conflicts with CLI argument.
The bridge URL hardcodes port 49134, but there's a --ws-port CLI argument (defaulting to 3112) that's passed to ServerConfig but not used for the bridge connection. This creates confusion about which port is actually used.
If these are intentionally different ports (bridge SDK port vs. frontend WebSocket proxy port), consider:
- Renaming the CLI arg to clarify its purpose (e.g.,
--ws-proxy-port) - Adding a separate
--bridge-portargument for the SDK connection - Adding a comment explaining the distinction
🔧 If bridge should use the CLI argument
- let bridge_url = format!("ws://{}:49134", args.engine_host);
+ let bridge_url = format!("ws://{}:{}", args.engine_host, args.ws_port);📝 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.
| // Initialize bridge connection to iii engine | |
| let bridge_url = format!("ws://{}:49134", args.engine_host); | |
| let bridge = iii_sdk::Bridge::new(&bridge_url); | |
| // Initialize bridge connection to iii engine | |
| let bridge_url = format!("ws://{}:{}", args.engine_host, args.ws_port); | |
| let bridge = iii_sdk::Bridge::new(&bridge_url); |
🤖 Prompt for AI Agents
In `@packages/console-rust/src/main.rs` around lines 79 - 81, The bridge URL
currently hardcodes port 49134 (bridge_url / iii_sdk::Bridge::new) while the CLI
provides --ws-port for ServerConfig, causing confusion; add a dedicated CLI
option (e.g., --bridge-port) and corresponding field in your args/ServerConfig
(bridge_port), parse it from CLI, and use that value when building bridge_url
(replace the hardcoded 49134 with args.bridge_port); alternatively if you intend
the existing --ws-port to be the bridge port, wire args.ws_port into bridge_url
instead and add a clarifying comment near bridge_url and the CLI flag to explain
the distinction.
| // Build CORS layer - permissive for local development | ||
| let cors = CorsLayer::new() | ||
| .allow_origin(Any) | ||
| .allow_methods(Any) | ||
| .allow_headers(Any); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/console-rust/src/server.rs | sed -n '115,135p'Repository: MotiaDev/iii-console
Length of output: 828
🏁 Script executed:
rg -n "CorsLayer|allow_origin|allowed_origins|CORS" packages/console-rust/src -C2Repository: MotiaDev/iii-console
Length of output: 871
🏁 Script executed:
rg -n "config\\.host|Config\s*{" packages/console-rust/src/server.rs | head -20Repository: MotiaDev/iii-console
Length of output: 173
🏁 Script executed:
fd server.rs packages/console-rust/srcRepository: MotiaDev/iii-console
Length of output: 99
🏁 Script executed:
rg -n "fn.*\{" packages/console-rust/src/server.rs | head -5Repository: MotiaDev/iii-console
Length of output: 184
🏁 Script executed:
sed -n '1,130p' packages/console-rust/src/server.rs | head -80Repository: MotiaDev/iii-console
Length of output: 2348
🏁 Script executed:
rg -n "struct Config|pub config" packages/console-rust/src -A 8Repository: MotiaDev/iii-console
Length of output: 46
🏁 Script executed:
cat packages/console-rust/src/server.rs | head -130Repository: MotiaDev/iii-console
Length of output: 3943
Restrict CORS to local addresses to prevent unauthorized cross-origin access.
Lines 121–125 configure CORS to accept any origin and any method/header. While the comment notes this is for "local development," the server can actually bind to non-local addresses (e.g., 0.0.0.0 or public IPs) via the ServerConfig parameter, exposing the API to arbitrary origins if not restricted.
Scope allowed origins to 127.0.0.1 and localhost only, or make it configurable to accept only expected client origins for production deployments.
Note: The suggested diff's check for 0.0.0.0 as local is incorrect—0.0.0.0 binds to all interfaces and should not receive permissive CORS.
🤖 Prompt for AI Agents
In `@packages/console-rust/src/server.rs` around lines 121 - 125, The CorsLayer
currently uses .allow_origin(Any)/.allow_methods(Any)/.allow_headers(Any) which
permits any origin; update the CORS configuration (the CorsLayer instantiation
where CorsLayer::new() is used and methods
allow_origin/allow_methods/allow_headers are called) to restrict allowed origins
to only "http://127.0.0.1" and "http://localhost" (and their secure variants if
needed) or wire it to a configurable list from ServerConfig so production
deployments can provide expected client origins; ensure you remove or replace
Any for allow_origin (and keep Any for methods/headers only if intended) and
document/validate that binding to 0.0.0.0 is not sufficient to justify
permissive CORS.
| | `/_console/streams/groups` | POST | List all state groups | | ||
| | `/_console/streams/group` | POST | List items in a state group | | ||
| | `/_console/streams/:stream/group/:group` | POST | Set a state item | | ||
| | `/_console/streams/:stream/group/:group/:key` | DELETE | Delete a state item | |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n 'streams/groups|streams/group' -g '*.rs' packages/console-rustRepository: MotiaDev/iii-console
Length of output: 46
🏁 Script executed:
git ls-files | grep -E '\.(rs|toml)$' | head -20Repository: MotiaDev/iii-console
Length of output: 403
🏁 Script executed:
fd -t f -e rs | head -30Repository: MotiaDev/iii-console
Length of output: 370
🏁 Script executed:
cat -n packages/console-rust/src/server.rsRepository: MotiaDev/iii-console
Length of output: 5215
🏁 Script executed:
rg -i 'streams|state' -g '*.rs' packages/console-rust -A 2Repository: MotiaDev/iii-console
Length of output: 9597
Update API documentation to match actual backend routes.
The documented endpoints use incorrect paths and HTTP methods. The actual Rust routes (from packages/console-rust/src/bridge/triggers.rs) are:
/_console/states/groups— GET (not POST)/_console/states/group— POST/_console/states/:group/item— POST (not/:stream/group/:group)/_console/states/:group/item/:key— DELETE (not/:stream/group/:group/:key)
The documentation incorrectly references /_console/streams/ paths, uses wrong HTTP methods, and has incorrect path parameter names.
🤖 Prompt for AI Agents
In `@README.md` around lines 261 - 264, The README API table is wrong: replace the
documented `/_console/streams/...` endpoints and methods with the actual Rust
routes from packages/console-rust/src/bridge/triggers.rs — change
`/_console/streams/groups` POST to `/_console/states/groups` GET, change
`/_console/streams/group` POST to `/_console/states/group` POST, change
`/_console/streams/:stream/group/:group` POST to `/_console/states/:group/item`
POST, and change `/_console/streams/:stream/group/:group/:key` DELETE to
`/_console/states/:group/item/:key` DELETE; update the table rows in README.md
accordingly and ensure path parameter names match `:group` and `:key`.
iii-console Architecture Refactor: Rust Binary + Embedded Frontend
Summary
This PR implements a complete architectural refactor of iii-console, transforming it from a Next.js-based application into a standalone Rust binary with an embedded React frontend. This change significantly improves deployment simplicity, reduces dependencies, and provides a single-binary distribution model.
Key Changes
console-frontend/(React + Vite) andconsole-rust/(Axum server + iii SDK bridge)rust-embedBreaking Changes
npx iii-consolenow runs the Rust binary instead of Node.js serveriii-example/- moved to separate repositoryType of Change
Architectural Changes
Before (Next.js)
After (Rust Binary)
New Features & Enhancements
Rust Binary Components
main.rs- CLI args parsing, bridge initialization, server startupserver.rs- Axum HTTP server with CORS, static file serving, API proxyingbridge/- iii SDK integration for registering console functions/triggersfunctions.rs- Console management functions (559 lines)triggers.rs- Console trigger registrationerror.rs- Unified error handlingFrontend Improvements
api/queries.ts)FlameGraph.tsx- CPU-style flame graph for span analysisWaterfallChart.tsx- Timeline waterfall visualizationTraceMap.tsx- Service dependency graphServiceBreakdown.tsx- Service-to-service call breakdownSpanPanel.tsx- Detailed span inspector with tabs (info, tags, logs, errors, baggage)API Organization
New domain-based API structure:
api/alerts/- Alert states, sampling rulesapi/events/- Functions, triggers, invocationapi/observability/- Logs (legacy + OTEL), metrics, tracesapi/state/- State groups/items, streamsapi/system/- Status, health, workers, adapters, configapi/queries.ts- TanStack Query options for all endpointsapi/websocket.ts- WebSocket connection managerBuild Optimizations
lto = true,opt-level = "z", stripped symbols)rust-embed(no separate asset serving)Testing
Tested with the following setup:
Terminal 1 - iii Engine:
cd /path/to/iii-engine cargo run --release -- --config config.yamlTerminal 2 - Console (Development):
Terminal 3 - Console (Production):
Verified Functionality
Migration Guide
For Users
Old usage:
New usage (after release):
# Download binary from releases chmod +x iii-console ./iii-console -p 3113 --engine-host localhostOr via npm (wrapper to Rust binary):
For Developers
Old development:
New development:
Dependencies
Added
axum,tower,tower-http,rust-embed,iii(SDK),anyhow,clap,serde_json,tokio@tanstack/router,@tanstack/react-query,vite,tailwindcss@4Removed
bin/cli.js)File Structure Changes
Removed:
src/app/**- Next.js App Router pagessrc/lib/api.ts- Monolithic API clientbin/cli.js- Node.js CLI wrappernext.config.ts,eslint.config.mjs,postcss.config.mjsiii-example/**- Example appAdded:
packages/console-frontend/**- React frontend (Vite + TanStack)packages/console-rust/**- Rust binary (Axum + iii SDK)pnpm-workspace.yaml- Monorepo configurationlefthook.yml- Pre-commit hooksReorganized:
src/lib/api.ts→packages/console-frontend/src/api/**/*src/components/→packages/console-frontend/src/components/src/app/*/page.tsx→packages/console-frontend/src/routes/*.tsxPerformance Improvements
Documentation Updates
Checklist
Additional Context
This refactor was motivated by several goals:
Future Work
Known Issues
None at this time. All functionality from the Next.js version has been preserved or improved.
Related Issues: N/A
Breaking Changes: Yes (deployment model, CLI usage)
Requires Migration: Yes (see Migration Guide above)
Summary by CodeRabbit
New Features
Infrastructure