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

226 lines
12 KiB
Markdown

# 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
```sh
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)
```sh
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.