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
243 changes: 177 additions & 66 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,207 @@
# AGENTS.md

## Purpose
This file defines how agents should implement changes in `signal-trace` using best practices for its actual stack: React 19 + TypeScript + Vite.
This file defines how agents should implement changes in `signal-trace` using best practices for its actual stack: React 19 + TypeScript + Vite + Vitest.

## Project Context
- Application type: browser-based WebSocket traffic inspector for IoT debugging.
- Main runtime file: `src/App.tsx`.
- Entry: `src/main.tsx` → `src/App.tsx` (thin orchestrator, ~160 lines).
- Build tool: Vite.
- Package manager: npm.
- Current scripts:
- `npm run dev`
- `npm run build`
- `npm run preview`
- Scripts:
- `npm run dev` — start Vite dev server
- `npm run build` — type-check (`tsc -b`) then bundle
- `npm run preview` — preview production build
- `npm run test` — Vitest watch mode
- `npm run test:run` — run all tests once

---

## Project Structure

```
src/
App.tsx # Thin orchestrator — wires hooks + components, owns handleSend/handleConnect/handleDisconnect/handleClear
types.ts # Shared domain types: TraceMessage, Metrics, SendParams, Direction, LinkState, SchemaValueType, SchemaPropertyDraft
main.tsx
styles.css
hooks/
useTimeline.ts # Messages, filtering, search, metrics, demo mode, replay, import/export
useConnection.ts # WebSocket lifecycle, Socket.IO handshake, protocol decode, RTT tracking
useSchemaGuard.ts # Schema builder state, parsedSchema memo, Esc key listener
components/
ConnectionPanel.tsx # Connection settings form (React.memo)
NamespaceFilter.tsx # Namespace toggle buttons (React.memo)
TransmitPanel.tsx # Send form with local state (React.memo)
SchemaGuardModal.tsx # Schema builder modal (React.memo)
MessageRow.tsx # Single message row with expand/copy (React.memo)
TimelinePanel.tsx # Right-panel shell composing MessageRow list (React.memo)
lib/
trace-utils.ts # Pure: frame decoding, schema validation, hex preview, safeJson
socketio-utils.ts # Pure: URL building, namespace/path normalization, auth parsing
tests/
hooks/ # renderHook tests for useTimeline, useConnection, useSchemaGuard
components/ # render tests for every component
app.test.tsx # Integration tests via full App render
trace-utils.test.ts
socketio-utils.test.ts
setup.ts
```

---

## Core Delivery Rules
- Make the smallest safe change that fully solves the task.
- Preserve current behavior unless the task explicitly changes it.
- Preserve existing behavior unless the task explicitly changes it.
- Do not refactor unrelated areas in the same patch.
- Prefer explicit, typed contracts over implicit assumptions.
- Keep code easy to debug in real-time traffic scenarios.

## Architecture and Boundaries
- Keep UI rendering concerns separate from protocol/decoding/validation logic.
- Extract pure utilities for:
- frame decoding
- payload parsing/normalization
- schema validation
- replay timing logic
- Avoid coupling transport formats directly to presentation components.
- Keep side effects (WebSocket connect/disconnect, timers, file import/export) localized and cleanup-safe.

## React 19 + Vite Best Practices
- Follow Vercel React guidance for performance-sensitive code paths.
- Re-render hygiene:
- use `useMemo`/`useCallback` only for expensive computations or stable callback identity needs
- avoid derived state in `useEffect` when it can be derived during render
- prefer functional `setState` updates when using previous state
- Effects:
- keep dependencies correct and as primitive/stable as possible
- move user-triggered logic into event handlers instead of effects
- always clean up WebSocket and timer side effects
- Bundle discipline:
- avoid unnecessary dependencies
- prefer direct imports over barrel patterns in hot paths
- gate optional/heavy features behind lazy loading when justified
- Rendering:
- keep long-list rendering efficient
- avoid extra object/array allocations in tight render loops unless needed for correctness
---

## Architecture Rules

### Hook responsibilities
| Hook | Owns |
|---|---|
| `useTimeline` | messages, activeNamespaces, search, expandedId, copiedId, demoMode, replaySpeed, all replay/import/export logic |
| `useConnection` | wsRef, connState, WS event handlers, Socket.IO handshake, protocolMode, pendingMapRef (RTT), all connection settings |
| `useSchemaGuard` | schemaProperties, schemaModalOpen, parsedSchema |

- Hooks accept only primitive props or stable callbacks — never raw state setters from another hook.
- `App.tsx` is the only place allowed to call more than one hook and combine their outputs.
- `sendMessage` / `handleSend` stays in `App.tsx` because it bridges all three hooks.
- `clearPending` must be called from `App.tsx`'s `handleClear` alongside `clearTimeline`.
- When disconnecting or toggling demo mode, call `stopReplay` in `App.tsx` before delegating to the hook.

### Component responsibilities
- Components receive data and callbacks via props — they hold no business logic.
- `TransmitPanel` is the only component that owns local UI state (form fields). It calls `onSend(SendParams)`.
- All list/panel components are wrapped in `React.memo`.

### Separation of concerns
- Pure utilities live in `src/lib/`. They have no React imports and are fully unit-testable.
- WebSocket side-effects live in `useConnection` only.
- Timer side-effects (demo interval, replay timeout) live in `useTimeline` only.
- File I/O (import/export) lives in `useTimeline` only.

---

## React 19 Best Practices

### Hooks
- Use `useCallback` for callbacks passed to memoized child components or used in `useEffect` deps.
- Use `useMemo` for expensive derivations (filtered list, metrics, parsedSchema).
- Avoid `useEffect` for derived state — compute it during render with `useMemo` instead.
- Use functional `setState` updates (`prev => ...`) whenever new state depends on previous.
- Keep `useEffect` dependency arrays minimal and correct; prefer stable refs for values that must not re-trigger effects.

### Stale closure pattern
- When a `useEffect`-created event handler (e.g., `ws.onmessage`) needs to call a function that depends on state, store the function in a `useRef` and update the ref via `useEffect`. The handler always reads `ref.current`. See `recordIncomingRef` in `useConnection.ts`.

### Memoization
- Wrap every component in `React.memo` that renders inside a list or receives stable-but-complex props.
- Do not memo components that always receive new props on every parent render (no benefit).
- Pass primitive values or `useCallback`-wrapped handlers as props to memoized components to keep them from unnecessary re-renders.

### State batching
- React 19 batches all state updates by default. Never call multiple setters and expect intermediate renders between them.
- When a test sets state before calling a function under test, use separate `act()` blocks: one for state, one for the action.

---

## TypeScript Standards
- Keep strict typing; do not introduce `any` unless unavoidable and documented.
- Model domain types explicitly (`TraceMessage`, protocol mode, payload format).
- Parse unknown input as `unknown`, then narrow with guards.
- Validate imported/exported data shape before storing in app state.
- Always use `strict: true`. Never introduce `any` unless unavoidable and annotated with a comment.
- Model domain types explicitly in `src/types.ts`. Add new shared types there, not inline in component files.
- Parse external input (`unknown`) with explicit type guards before storing in state.
- Use `import type` for type-only imports to keep runtime output clean.
- Prefer `interface` for public API shapes, `type` for union/intersection aliases.

---

## WebSocket and Protocol Handling
- All WS event handlers (`onopen`, `onclose`, `onerror`, `onmessage`) are set once at connection time in `useConnection.connect()`.
- `protocolMode` changes after connection are handled via the `recordIncomingRef` pattern — do not recreate the WS or re-attach handlers for mode changes.
- Socket.IO handshake sequence: engine open (`0...`) → send namespace connect (`40<ns>`) → await namespace ready (`40<ns>` or `40<ns>,`) → set `socketIoReadyRef.current = true` → `CONNECTED`.
- Engine.IO ping (`2`) is handled automatically with pong (`3`) in `onmessage`.
- Always clean up: call `ws.close()` and null `wsRef.current` in both `disconnect()` and the unmount cleanup effect.

---

## CSS and Layout
- Styling is in a single `src/styles.css` — no CSS modules or Tailwind.
- CSS variables: `--bg`, `--panel`, `--line`, `--line-strong`, `--text`, `--muted`, `--hazard`, `--good`, `--err`, `--info`.
- Layout: `.shell` fills `100dvh` with `grid-template-rows: auto 1fr`. `.layout` is a two-column grid (350px sidebar + 1fr timeline). Both have `min-height: 0; overflow: hidden` to enable child scroll.
- `.timeline` is a flex column with `min-height: 0; overflow: hidden`. `.rows` has `flex: 1; overflow: auto` — this is what actually scrolls.
- `.controls` (sidebar) has `overflow-y: auto` to scroll independently.
- Use `.hud` for the corner-bracket HUD border effect.
- Button variants: plain (default), `.btn-primary` (accent border), `.btn-active` (amber active state).
- Status dot variants: `.dot.disconnected`, `.dot.connecting` (pulse), `.dot.connected` (breathe), `.dot.error`.

---

## Testing Standards

### Stack
- Vitest + `@testing-library/react` + `@testing-library/user-event` + `@testing-library/jest-dom`.
- Setup file: `tests/setup.ts` (imports `@testing-library/jest-dom/vitest`).
- Environment: jsdom.

### Coverage requirements
- Every new hook must have a `tests/useXxx.test.ts` using `renderHook` + `act`.
- Every new component must have a `tests/ComponentName.test.tsx` using `render` + user events.
- Integration flows that span multiple hooks should be tested in `tests/app.test.tsx`.
- Pure utilities in `src/lib/` must have dedicated unit tests.

### Patterns
- Mock `WebSocket` with a `MockWebSocket` class that exposes `emitOpen()`, `emitMessage(data)`, `emitError()`, and `emitClose()` helpers.
- Mock `navigator.clipboard.writeText` with `vi.fn().mockResolvedValue(undefined)` in `beforeEach`.
- Mock `URL.createObjectURL` / `URL.revokeObjectURL` for export tests.
- Use `vi.useFakeTimers()` / `vi.useRealTimers()` for demo mode and replay timer tests.
- State setters and the action that reads updated state must be in **separate** `act()` blocks.
- Use `within(container)` to scope queries when multiple similar elements exist (e.g., checkboxes inside a modal vs. in the background).
- Always `cleanup()` in `afterEach`.

### What not to test
- Internal ref values — test observable behavior (state, rendered output, callbacks fired).
- CSS class names beyond the functional ones (`.expanded`, `.ns-btn--active`, direction classes).
- Implementation details of third-party libraries.

---

## Data Validation and Safety
- Treat all incoming WebSocket frames and imported files as untrusted input.
- Fail fast on invalid JSON/schema with user-visible, actionable errors.
- Keep boundary validation close to IO operations.
- Never log secrets or sensitive tokens from payloads.
- Treat all incoming WebSocket frames and imported files as untrusted (`unknown`).
- Validate imported timeline entries in `normalizeImported` before pushing to state.
- Never log or display raw auth tokens from Socket.IO auth payloads in the timeline.
- Fail fast with a system-event message for recoverable errors (connect failure, schema violation, import parse error).

---

## Error Handling and Observability
- Do not swallow errors silently.
- Emit system events/messages for recoverable issues (connect errors, schema violations, import failures).
- Include enough context in diagnostics to reproduce issues (event, namespace, protocol mode).
- Use `appendSystem(event, payload)` to emit diagnostic messages into the timeline.
- Include event name, namespace, and protocol mode in error context where relevant.

---

## UX and Accessibility
- Preserve keyboard-accessible controls and semantic form elements.
- Keep loading/empty/error/success states explicit in the UI.
- Maintain readable contrast and clear status indicators for connection state.

## Testing and Verification
- For each behavior change, add or update tests where test infrastructure exists.
- Until automated tests are added, perform focused manual verification:
- connect/disconnect lifecycle
- message send/receive in all protocol modes (`auto`, `raw`, `socketio`)
- schema validation success/failure paths
- import/export JSON and NDJSON
- timeline replay and cancel behavior
- Always run `npm run build` before finishing substantial changes.
- All interactive controls must be reachable via keyboard.
- Use semantic HTML: `<button>`, `<label>`, `<input>`, `<select>`, `<details>`/`<summary>`, `<article>` for message rows.
- Connection state must always be visible via the status dot + label in the Connection panel summary.
- Keep empty, loading, and error states explicit — no blank panels.

---

## Git and Change Hygiene
- Keep commits focused and descriptive.
- In PR/task summary, include:
- what changed
- why it changed
- risks/regression areas
- how it was verified
- In a PR/task summary include: what changed, why, regression areas, and how it was verified.

---

## Definition of Done
- Requirements implemented and behavior validated.
- Build succeeds via `npm run build`.
- New edge cases and failure paths are handled.
- README/docs updated if workflow or behavior changed.
- No obvious type or runtime regressions introduced.
- `npm run build` succeeds with no type errors.
- `npm run test:run` passes — all tests green.
- New behavior has corresponding tests.
- No regressions in existing tests.
- README updated if the user-visible workflow or feature set changed.
76 changes: 54 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,39 +61,71 @@ If your backend uses Socket.IO namespaces/auth:
- Signal Trace waits for namespace connection before allowing Socket.IO sends.
- Engine.IO ping (`2`) is handled automatically with pong (`3`) keepalive replies.

### Repeated Sends To `bewf`
When sending multiple messages to `bewf`, keep **Auto-refresh id/timestamp** enabled in the **Transmit** panel.
This updates top-level `id` and `timestamp` on each send and helps avoid backend event key collisions when re-sending the same payload.

Recommended `bewf` transmit settings:
- **Protocol Decode**: `socketio`
- **Socket.IO Namespace**: `/devices`
- **Socket.IO Event**: `device_telemetry`

### Schema Guard Builder
- Open **Schema Guard** from the sidebar and click **Open Schema Builder**.
- Add fields with type (`string`, `number`, `boolean`, `object`, `array`).
- Use the **Required** checkbox next to each field to mark it required.
- Leave all fields empty to disable schema validation.

## Testing
Current test suite covers:
- Socket.IO URL/auth/namespace utility behavior
- Frame decoding and schema-validation utility behavior
- Core UI flows (schema modal, validation errors, Socket.IO send guards, handshake lifecycle)

Stack: **Vitest** + **@testing-library/react** + **@testing-library/user-event** + **@testing-library/jest-dom**. Test files live next to their implementation files.

Run all tests once:
```bash
npm run test:run
```

Run in watch mode:
```bash
npm run test
```

Coverage areas:
- `src/lib/` — pure utility unit tests (frame decoding, schema validation, Socket.IO URL/auth/namespace helpers)
- `src/hooks/` — `renderHook` + `act` tests for `useTimeline`, `useConnection`, `useSchemaGuard`
- `src/components/` — render + user-event tests for every component
- `src/App.test.tsx` — integration tests: schema modal, validation errors, Socket.IO handshake lifecycle

## Project Structure
```text
.
├── src/
│ ├── App.tsx # Main inspector UI + runtime logic
│ ├── main.tsx # App bootstrap
│ └── styles.css # Styling
├── index.html
├── package.json
└── AGENTS.md
src/
App.tsx # Thin orchestrator — wires hooks + components
App.test.tsx # Integration tests
types.ts # Shared domain types
main.tsx
styles.css
hooks/
useTimeline.ts # Messages, filtering, search, metrics, demo mode, replay, import/export
useTimeline.test.ts
useConnection.ts # WebSocket lifecycle, Socket.IO handshake, protocol decode, RTT tracking
useConnection.test.ts
useSchemaGuard.ts # Schema builder state, parsedSchema memo, Esc key listener
useSchemaGuard.test.ts
components/
ConnectionPanel.tsx # Connection settings form
ConnectionPanel.test.tsx
NamespaceFilter.tsx # Namespace toggle buttons
NamespaceFilter.test.tsx
TransmitPanel.tsx # Send form with local state
TransmitPanel.test.tsx
SchemaGuardModal.tsx # Schema builder modal
SchemaGuardModal.test.tsx
MessageRow.tsx # Single message row with expand/copy
MessageRow.test.tsx
TimelinePanel.tsx # Right-panel shell composing MessageRow list
TimelinePanel.test.tsx
lib/
trace-utils.ts # Pure: frame decoding, schema validation, hex preview, safeJson
trace-utils.test.ts
socketio-utils.ts # Pure: URL building, namespace/path normalization, auth parsing
socketio-utils.test.ts
vitest.setup.ts # @testing-library/jest-dom setup
vite.config.ts
index.html
package.json
AGENTS.md # Architecture and development guidelines
```

## Notes
- This project includes a Vitest + Testing Library test suite (`npm run test:run`).
- If your endpoint is unavailable, use Demo mode to test the UI behavior.
Loading