Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/RandomCodeSpace/otelcontext

go 1.25.10
go 1.25.11

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
Expand Down
10 changes: 10 additions & 0 deletions internal/api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ func (rw *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
return rw.ResponseWriter.(http.Hijacker).Hijack()
}

// Flush implements http.Flusher so SSE streams (the MCP GET endpoint) can push
// events through the middleware. Embedding the http.ResponseWriter interface
// drops Flush from the method set, so without this the MCP SSE handler's
// w.(http.Flusher) assertion fails and it returns "SSE not supported".
func (rw *responseWriter) Flush() {
if f, ok := rw.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}

// MetricsMiddleware records OtelContext_http_requests_total and OtelContext_http_request_duration_seconds
// for every HTTP request.
func MetricsMiddleware(metrics *telemetry.Metrics, next http.Handler) http.Handler {
Expand Down
6 changes: 4 additions & 2 deletions internal/api/views/views.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ type DashboardStats struct {
AvgLatencyMs float64 `json:"avg_latency_ms"`
ErrorRate float64 `json:"error_rate"`
ActiveServices int64 `json:"active_services"`
P99Latency int64 `json:"p99_latency"`
P99LatencyMs float64 `json:"p99_latency_ms"`
TopFailingServices []ServiceError `json:"top_failing_services"`
}

Expand Down Expand Up @@ -330,7 +330,9 @@ func DashboardStatsFromModel(s *storage.DashboardStats) DashboardStats {
AvgLatencyMs: s.AvgLatencyMs,
ErrorRate: s.ErrorRate,
ActiveServices: s.ActiveServices,
P99Latency: s.P99Latency,
// storage.P99Latency is microseconds (storage tests assert µs); convert
// to milliseconds here so the API matches AvgLatencyMs and the field name.
P99LatencyMs: float64(s.P99Latency) / 1000.0,
}
if len(s.TopFailingServices) > 0 {
out.TopFailingServices = make([]ServiceError, len(s.TopFailingServices))
Expand Down
4 changes: 4 additions & 0 deletions internal/ui/dist/assets/MCPConsoleView-CH5VtsjS.js

Large diffs are not rendered by default.

321 changes: 321 additions & 0 deletions internal/ui/dist/assets/ServicesView-Bb9CYmXY.js

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion internal/ui/dist/assets/cytoscape-cose-bilkent-CEIBo6Gj.js

This file was deleted.

321 changes: 0 additions & 321 deletions internal/ui/dist/assets/cytoscape.esm-Dm6iss-N.js

This file was deleted.

1 change: 0 additions & 1 deletion internal/ui/dist/assets/design-system-BNhP-Tae.js

This file was deleted.

1 change: 0 additions & 1 deletion internal/ui/dist/assets/design-system-DFjB0sSn.js

This file was deleted.

1 change: 0 additions & 1 deletion internal/ui/dist/assets/design-system-IOKLDoaG.js

This file was deleted.

9 changes: 0 additions & 9 deletions internal/ui/dist/assets/index-B9ZFj2IV.js

This file was deleted.

10 changes: 10 additions & 0 deletions internal/ui/dist/assets/index-UdCF7Wgf.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions internal/ui/dist/assets/inputs-DxVBbFvb.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions internal/ui/dist/assets/useMediaQuery-D3Mg-H7H.js

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion internal/ui/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OtelContext</title>
<script type="module" crossorigin src="/assets/index-B9ZFj2IV.js"></script>
<script type="module" crossorigin src="/assets/index-UdCF7Wgf.js"></script>
<link rel="modulepreload" crossorigin href="/assets/useMediaQuery-D3Mg-H7H.js">
<link rel="stylesheet" crossorigin href="/assets/index-DzLWOk_K.css">
</head>
<body>
Expand Down
40 changes: 40 additions & 0 deletions ui/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import js from '@eslint/js'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'

export default tseslint.config(
{ ignores: ['dist', 'node_modules'] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2022,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
// The two react-compiler-era rules below fire on legitimate, codebase-wide
// patterns rather than real defects, so they are warnings (visible) not
// errors:
// - set-state-in-effect: the async data-fetch hooks setState AFTER an
// await (the pre-existing useSystemGraph/useDashboard do the same); the
// rule can't follow the await boundary.
// - refs: the intentional state->ref mirror in useWebSocket and the
// imperative cytoscape hover-overlay positioning in ServiceGraph.
// rules-of-hooks and exhaustive-deps stay at their recommended levels.
'react-hooks/set-state-in-effect': 'warn',
'react-hooks/refs': 'warn',
},
},
)
51 changes: 31 additions & 20 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
import { useState } from 'react'
import { AppShell } from '@ossrandom/design-system'
import { lazy, Suspense, useState } from 'react'
import { AppShell, Spin } from '@ossrandom/design-system'
import TopNav, { type OtelView } from './components/nav/TopNav'
import ServicesView from './components/observability/ServicesView'
import { useSystemGraph } from './hooks/useSystemGraph'
import { useDashboard } from './hooks/useDashboard'
import DashboardView from './components/dashboard/DashboardView'
import { useWebSocket } from './hooks/useWebSocket'
import type { Theme } from './hooks/useTheme'

export default function App() {
const [view, setView] = useState<OtelView>('services')
// Dashboard is the default view and loads eagerly. The Service Map pulls in
// cytoscape (~434 KB) and the MCP console is a large secondary surface — both
// are code-split so they don't weigh down the initial dashboard-first load.
const ServicesView = lazy(() => import('./components/observability/ServicesView'))
const MCPConsoleView = lazy(() => import('./components/mcp/MCPConsoleView'))

const graph = useSystemGraph()
const dash = useDashboard()
interface AppProps {
theme: Theme
onToggleTheme: () => void
}

export default function App({ theme, onToggleTheme }: Readonly<AppProps>) {
const [view, setView] = useState<OtelView>('dashboard')

// WebSocket retained as the live/offline source for the header indicator;
// log batches it pushes are intentionally discarded.
// WebSocket retained purely as the live/offline source for the header badge;
// the pushed log batches are intentionally discarded.
const ws = useWebSocket(() => undefined)
const wsConnected = !!ws.current
const wsConnected = ws.status === 'connected'

return (
<AppShell
header={
<TopNav view={view} onNavigate={setView} wsConnected={wsConnected} />
<TopNav
view={view}
onNavigate={setView}
wsConnected={wsConnected}
theme={theme}
onToggleTheme={onToggleTheme}
/>
}
>
<ServicesView
graph={graph.graph}
loading={graph.loading}
error={graph.error}
dashboard={dash.dashboard}
stats={dash.stats}
/>
<Suspense fallback={<Spin label="Loading…" />}>
{view === 'dashboard' && <DashboardView onNavigate={setView} />}
{view === 'services' && <ServicesView />}
{view === 'mcp' && <MCPConsoleView />}
</Suspense>
</AppShell>
)
}
28 changes: 28 additions & 0 deletions ui/src/components/common/Truncate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { CSSProperties } from 'react'

interface TruncateProps {
readonly text: string
readonly style?: CSSProperties
}

// Layout-only inline styles (sanctioned escape hatch). `minWidth: 0` is the
// flex/grid fix that lets a shrinkable child ellipsize instead of overflowing.
const baseStyle: CSSProperties = {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
minWidth: 0,
display: 'block',
}

/**
* Single-line ellipsis with a native title tooltip on hover. Use for service
* names, log bodies, trace ids, operation names — anything that can overflow.
*/
export default function Truncate({ text, style }: TruncateProps) {
return (
<span title={text} style={{ ...baseStyle, ...style }}>
{text}
</span>
)
}
Loading