Files
go-otel/AGENTS.md
T
matsandClaude Sonnet 5 26b677154b
Build and Publish / Metadata (push) Successful in 2s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 7s
dev-auth: add 'get' command; rewrite AGENTS.md dev-auth as quick start + gotchas
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 09:18:06 +00:00

12 KiB

Agent instructions

Instructions for AI agents (and contributors) working in this repo. Currently covers the otel-ui frontend design system, since that's the active area of work; add sections here as other parts of the repo get their own conventions.

otel-ui: design system

The rule

One theme, light and dark - that's it. otel-ui/src/index.css defines a single set of --ds-* tokens with a dark variant (default) and a light variant (:root[data-theme="light"]). Never introduce a second palette, a third theme variant, or a new token namespace alongside ds-*. If a screen needs a color, it comes from this table or it doesn't exist yet - extend the table, don't invent a one-off.

The theme toggle lives in TopBar.tsx, persists to localStorage (otel.ui.theme), and is applied before first paint by an inline script in index.html so there's no flash of the wrong theme.

The old MD3-style --color-surface-container-* / --color-on-surface tokens, the Material Symbols icon font and the Geist/Inter/JetBrains fonts have been deleted. Never reintroduce them, raw Tailwind palette colors (slate-*, sky-*, rose-*, white/10...), gradients, or shadows. Use lucide-react for icons.

Documented exceptions (data-viz only, never chrome): per-service span colors in the trace waterfall (getSpanColor in otel-utils.ts) and the muted chart-series hues for metric lines/pods (POD_COLORS).

Token reference

Source of truth is otel-ui/src/index.css (:root and :root[data-theme="light"]). Tailwind utilities are generated as bg-ds-*, text-ds-*, border-ds-*.

Token Dark Light Used for
ds-bg #0D1014 #F4F2ED page ground
ds-chrome #0B0E12 #FFFFFF icon rail, top bar
ds-surface #14181E #FFFFFF cards, panels
ds-surface-2 #191E26 #F7F5F0 inputs, chips, selected row
ds-fill #1D232B #EDEAE3 hairlines inside cards, chart bands, bar tracks
ds-line #242B34 #E3E0D8 card/control borders
ds-line-strong #2A313B #D6D2C8 chip borders, neutral badges
ds-text #E8ECF2 #161A1F primary text
ds-text-2 #98A3B2 #4F5964 secondary text
ds-text-3 #8B96A6 #6B7480 labels, metadata, muted text - this is the accessibility floor, don't go dimmer
ds-critical / ds-critical-bg #FF6F5E / #2B1512 #C4402C / #FBE9E5 breached threshold, open errors
ds-warn / ds-warn-bg #F0B24A / #2A2011 #8A5D0E / #F9EFDA degraded but not (necessarily) user-visible
ds-ok #4FCF9A #1F7A56 healthy, resolved, recovered
ds-accent / ds-accent-bg #7B8CFF / #161B33 #3B4FC4 / #E9ECFB the one interactive accent - primary buttons, selected segment, links, focus
ds-on-accent ds-chrome value #FFFFFF text on top of ds-accent

Colour is a signal, not decoration. Chrome, borders and type are neutral. ds-critical / ds-warn / ds-ok only appear where a real measurement is out of its expected band or a real state changed (open vs resolved). A severity pill's color must correspond to an actual state, never picked for visual variety. Don't tint cards, headers or buttons "for interest."

Fonts

Three families (otel-ui/index.html loads them; index.css exposes them as --font-display / --font-ui / --font-data, used via Tailwind's font-[family-name:var(--font-display)] arbitrary syntax):

  • Bricolage Grotesque (--font-display) - page/card headings, big metric numbers.
  • Instrument Sans (--font-ui) - all other UI text and body copy. Also the new global default (index.css's @theme overrides --font-body), so unmigrated screens already inherit this typeface even before their colors move.
  • IBM Plex Mono (--font-data) - anything a person would copy or grep: ids, key=value attribute pairs, timestamps, durations, service names.

Never Inter, Roboto, Arial, or a new font stack.

Layout shell (already built - reuse, don't rebuild)

  • otel-ui/src/components/layout/IconRail.tsx - 72px fixed icon rail, global nav, one aria-label per icon.
  • otel-ui/src/components/layout/TopBar.tsx - 58px fixed top bar: search, time range, theme toggle, auth.
  • otel-ui/src/components/FilterBar.tsx - generic filter bar (optional scope segmented control, removable chips, dashed add-filter, an always-visible match readout, clear-all). Reuse this on every screen that lists things instead of building a bespoke filter row.
  • App.tsx wires the shell globally and renders the single page-level <main>. Screens render inside it - see pitfall #2 below.

Screen pattern

Established by IssuesVisualizer.tsx (the triage overview) and RumSessionsVisualizer.tsx (real-user sessions) - follow this shape for the remaining screens:

  • <FilterBar /> at the top of the screen's content.
  • A main content column (flex-1) plus an optional fixed-width aside (~340-420px) for supporting stats/panels.
  • Cards: rounded-[14px] border border-ds-line bg-ds-surface, no shadows, no gradients - depth comes from the border and ground color alone.
  • No per-service rainbow badges. getServicePalette in otel-utils.ts (the gradient hash-to-color picker) is legacy: per the original design spec, a muted per-service palette is reserved for trace-waterfall span coloring only, never for general UI chrome like service badges. Use a neutral border-ds-line-strong text-ds-text-2 badge instead.
  • Real <button> / <a href> / <input> + <label> only. Never onClick on a div or span[role=button]. aria-label on every icon-only control; aria-hidden="true" on decorative SVG icons (use lucide-react, not the Material Symbols icon font - see pitfall below).

Pitfalls hit migrating Issues and RUM sessions - avoid repeating these

  1. No nested <button> elements. Browsers implicitly close the outer <button> the moment they hit an inner one, which silently breaks click handling on whatever's after it in the DOM (this shipped once, undetected until manual testing). If a row/card needs both "click to expand" and inner action buttons, make the expandable area its own <button> and put the action buttons as siblings, not children.
  2. No nested <main> landmarks. App.tsx already renders one <main>; a screen component must not render its own - use a <div>.
  3. whitespace-nowrap + shrink-0 on fixed-height pills/badges. Without them, a longer label silently wraps onto two lines and blows out the pill's height inside its flex row - easy to miss until you test with real (longer) data or a narrower viewport.
  4. Never fabricate data. Before adding a metric, column, or button, check whether the field actually exists in the real data model - the backend API response, or for RUM specifically, what static/otel-rum.js actually sends (see the header comment in otel-ui/src/lib/rum-utils.ts for a worked example of what's real vs. invented). If a design reference shows something with no backing data - the RUM mockup invented rage clicks, geo location, a "frustration score", and session replay, none of which this pipeline captures - drop it or replace it with the closest real, honestly-labeled equivalent. Never wire up a control that looks functional but isn't backed by anything.
  5. Screenshots of a hidden Browser preview pane are unreliable for position: fixed elements (stale compositor frame, even after a full reload). If a screenshot shows something that contradicts getComputedStyle, trust the computed style, front the tab, and re-check before concluding there's a real bug.

Migration status

Whole UI is on ds-* (one theme, light + dark). The nav strip in Tabs.tsx is gone - IconRail is the only navigation; Tabs.tsx just renders the active screen. TelemetryFilters is built on FilterBar. The Material Symbols icons were replaced with lucide.

Lessons from getting here:

  • Merges can silently revert a redesign. 67ce419 reverted IssuesVisualizer.tsx to the old styling (a parallel branch had edited the same file). After any merge touching otel-ui/src, run the greps below - they must return nothing.
  • A stale otel-ui/dist or un-redeployed image hides changes. The Go server serves otel-ui/dist, else ./dist, else the legacy root index.html. Rebuild (npm --prefix otel-ui run build) before concluding a screen isn't wired.
  • Still to polish (functional, but not yet re-laid-out to the screen pattern): TraceVisualization, MetricsVisualizer, StructuredLogTerminal, SearchAndWatcher, TrendsVisualizer, SetupPanel. They use ds-* colors via a mechanical token swap but keep their old layout/type scale, use onClick on non-buttons in places, and CommandAndQueryBar / WaveformHistogram still sit above every tab except Issues and RUM sessions (isTriageOverview in App.tsx). Restructure them onto FilterBar + the card/aside pattern when touching them.

Finding old-pattern usages

grep -rlE "surface-container|text-on-surface|primary-container|bg-error-container|material-symbols-outlined" otel-ui/src
grep -rnE "(bg|text|border|ring|from|to|via)-(slate|zinc|gray|sky|cyan|blue|violet|indigo|purple|emerald|teal|rose|red|amber|orange)-[0-9]|text-white|border-white|bg-(white|black)/" otel-ui/src

Both must print nothing.

Working order from here

Initial pass is done. Take one view at a time to the full screen pattern (FilterBar + card/aside layout, real buttons, fold or restyle the shared chrome for that screen), verify it in the browser in both themes, commit, then move to the next. Suggested order: Traces, Logs, Metrics, Search, Trends, Setup.

Local / agent development without login blocking

Quick start (agents: do this first)

node scripts/dev-auth.mjs get /api/issues     # signs in if needed, then GETs

If not signed in it prints Approve in a browser: https://users.k6n.net/device?user_code=XXXX-XXXX. Stop and give the human that link, then wait - the script polls (~10 min window) and continues on its own. Refresh token is stored in ~/.go-auth/dev/ (0700/0600), so this happens once, not per command. Other commands: login | token | whoami | forget. For raw curl: curl -H "Authorization: Bearer $(node scripts/dev-auth.mjs token)" https://otel.k6n.net/api/.... Env: OTEL_URL, AUTH_DEV_BASE, AUTH_DEVICE_CLIENT_ID (default dev-console), GO_AUTH_TOKEN (explicit-token escape hatch, never store).

Browser (npm run dev): with no session a "Dev sign-in" banner shows the same link + code and reloads once approved. Code is src/dev/device-auth.ts, loaded only under import.meta.env.DEV; /dev-auth/* is proxied by vite.config.ts to the issuer's /api/auth/*.

Gotchas (each cost a failed attempt)

  • Request encoding: /api/auth/device and /api/auth/device/token take form-encoded bodies (RFC 8628); only /api/auth/device/refresh takes JSON ({"refresh_token": ...}). JSON to the first two gives a fast invalid_grant that looks like a denied approval but is a malformed request.
  • 401 token not issued for this client: the otel server rejects the token's aud. It must contain otel-debug or OTEL_OIDC_AUDIENCE (https://otel.k6n.net, set in k8s/deployment.yaml). If it 401s, the deployed server predates that setting - redeploy with build.sh.
  • Never probe the issuer or auth CRDs with ad-hoc curl/kubectl unless the user has said to; the harness blocks it. The script above is the sanctioned path.

CRD setup (already applied; reference)

Reference docs: https://users.k6n.net/dev-auth.md, https://users.k6n.net/crds.md. k8s/auth-dev.yaml defines an AuthClient dev-console (redirectURIs: [] is required even for device-only clients; spec.audience: [https://otel.k6n.net]) plus AuthServer.spec.deviceFlow.

  • audience entries must be absolute URLs. otel-debug was rejected by the operator and flipped the AuthServer to READY False until fixed. --dry-run=server does not catch this; after applying, always check kubectl get authserver -n ecommerce (READY True) and kubectl describe authserver auth -n ecommerce (events).
  • Apply: kubectl apply --server-side --field-manager=otel-dev-auth -f k8s/auth-dev.yaml (a non-fatal "spec.issuer: Required value" warning for the partial AuthServer apply is expected).
  • Never commit tokens, persist them in a production build, or log them.