diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..b890e20 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,9 @@ +#!/bin/sh + +set -eu + +echo "Running lint..." +npm run lint + +echo "Running typecheck..." +npm run typecheck diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 72b60a6..f69581f 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -6,7 +6,7 @@ on: - '**' jobs: - lint-and-test: + lint: runs-on: ubuntu-latest steps: @@ -25,8 +25,60 @@ jobs: - name: Lint run: npm run lint + typecheck: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Typecheck + run: npm run typecheck + + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + - name: Test run: npm run test:run + build: + runs-on: ubuntu-latest + needs: [lint, typecheck, test] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + - name: Build run: npm run build diff --git a/README.md b/README.md index de5146e..fac5751 100644 --- a/README.md +++ b/README.md @@ -2,130 +2,178 @@ # Signal Trace -Signal Trace is a browser-based WebSocket traffic inspector for real-time IoT debugging. -It helps you connect to a WebSocket endpoint, inspect inbound/outbound messages, validate payloads against a simple schema, and replay/export traffic timelines. +Signal Trace is a browser-based WebSocket traffic inspector for real-time IoT debugging. It helps you connect to raw WebSocket or Socket.IO endpoints, inspect inbound and outbound frames, validate outgoing JSON against a lightweight schema, and replay or export captured traffic. ## Tech Stack + - React 19 - TypeScript - Vite +- Vitest +- Aegis Design System ## Features -- Live WebSocket connect/disconnect workflow -- Protocol decode modes (`auto`, `raw`, `socket.io`) -- Traffic timeline with direction, namespace, event, payload, bytes, and latency -- Search (in timeline header) and namespace filtering -- Visual Schema Guard constructor (modal) for outgoing payload validation -- Timeline import/export (`JSON` and `NDJSON`) -- Timeline replay with speed controls -- Demo mode for local experimentation + +- Live connect and disconnect workflow for WebSocket endpoints +- Protocol decode modes: `auto`, `raw`, and `socketio` +- Socket.IO handshake support with configurable path, namespace, auth JSON, and event name +- Timeline inspection with direction, namespace, event, payload, byte size, and latency +- Search and namespace filtering for narrowing noisy traffic +- Outbound payload helpers for JSON formatting and auto-refreshing `id` / `timestamp` +- Schema Guard modal for validating outgoing JSON before send +- Timeline import and export in `JSON` and `NDJSON` +- Replay controls with adjustable playback speed +- Demo mode for synthetic traffic generation without a backend +- Theme toggle in the app shell ## Requirements -- Node.js 20+ (recommended current LTS) + +- Node.js 20+ - npm 10+ ## Getting Started + 1. Install dependencies: + ```bash npm install ``` -2. Start development server: + +2. Start the development server: + ```bash npm run dev ``` -3. Open the local URL shown in terminal (typically `http://localhost:5173`). -## Available Scripts -- `npm run dev` - Start Vite dev server -- `npm run build` - Type-check and build production assets -- `npm run preview` - Preview the production build locally -- `npm run test` - Run tests in watch mode -- `npm run test:run` - Run all tests once +3. Open the local URL printed by Vite, usually `http://localhost:5173`. + +## Scripts + +- `npm run dev` - start the Vite dev server +- `npm run typecheck` - run the TypeScript project build without bundling +- `npm run build` - run type-checking and build production assets +- `npm run preview` - preview the production build locally +- `npm run lint` - run ESLint +- `npm run lint:fix` - run ESLint with fixes +- `npm run test` - run Vitest in watch mode +- `npm run test:run` - run the full test suite once ## Basic Usage -1. Set WebSocket URL (default: `ws://localhost:8080`). -2. Choose protocol mode (`auto` is recommended initially). -3. Click **Connect**. -4. Inspect incoming/outgoing traffic in the timeline. -5. Use timeline search and namespace filters to narrow results. -6. Optionally validate outgoing JSON payloads with the schema panel. -7. Export or replay timeline data as needed. - -### Socket.IO Auth Handshake -If your backend uses Socket.IO namespaces/auth: -- Enable **Socket.IO Handshake**. -- Set **Socket.IO Path** (commonly `/socket.io` or `/ws`). -- Set **Socket.IO Namespace** (for example `/devices` or `/frontend`). -- Provide **Socket.IO Auth JSON** when required (for example `{"serial":"...","token":"..."}`). -- Use protocol mode `socketio` to send event frames, and set **Socket.IO Event** if you need an event name other than `trace`. -- Signal Trace waits for namespace connection before allowing Socket.IO sends. -- Engine.IO ping (`2`) is handled automatically with pong (`3`) keepalive replies. - -### 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. + +1. Enter a WebSocket endpoint such as `ws://localhost:8080`. +2. Choose a protocol decode mode. Start with `auto` unless you know the framing. +3. Click `Connect`. +4. Inspect incoming and outgoing traffic in the timeline. +5. Use search and namespace filters to isolate the frames you care about. +6. Open `Schema Builder` if you want to validate outgoing JSON before sending. +7. Use replay, import, and export controls from the timeline toolbar as needed. + +## Sending Frames + +- The transmit panel defaults to the `/telemetry` namespace and a JSON payload. +- `Format JSON` prettifies valid JSON and emits a system event if the payload is invalid. +- `Auto-refresh id/timestamp` updates those fields before send when they already exist in the payload. +- If no correlation id is present, Signal Trace injects a `requestId` automatically before sending. + +## Socket.IO Handshake + +If your backend uses Socket.IO namespaces or auth: + +- Enable `Socket.IO Handshake`. +- Set `Socket.IO Path`, typically `/socket.io`. +- Set `Socket.IO Namespace`, for example `/devices`. +- Provide `Socket.IO Auth JSON` when your server expects auth in the namespace connect payload. +- Switch protocol mode to `socketio` for outbound event frames. +- Optionally set `Socket.IO Event`; the default event name is `trace`. +- Signal Trace waits for namespace readiness before allowing Socket.IO sends. +- Engine.IO ping frames (`2`) are answered automatically with pong (`3`). + +## Schema Guard + +- Open the schema builder from the transmit panel. +- Add fields with type `string`, `number`, `boolean`, `object`, or `array`. +- Mark fields as required when needed. +- Leave the schema empty to disable validation. +- Schema parse or validation failures are written to the system timeline instead of being swallowed. + +## Timeline Import, Export, and Replay + +- Export captured traffic as formatted `JSON` or line-delimited `NDJSON`. +- Import previously captured files in either format. +- Invalid or empty imports produce a system event instead of silently failing. +- Replay replays imported or captured messages in timestamp order with adjustable speed. +- Demo mode stops replay before starting synthetic traffic. ## Testing -Stack: **Vitest** + **@testing-library/react** + **@testing-library/user-event** + **@testing-library/jest-dom**. Test files live next to their implementation files. +Signal Trace uses Vitest with Testing Library and `@testing-library/jest-dom`. + +Run the full test suite once: -Run all tests once: ```bash npm run test:run ``` -Run in watch mode: +Run tests 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 +Coverage is organized around the current source layout: + +- `src/lib/*.test.ts` for pure utility helpers +- `src/hooks/*.test.ts` for hook behavior via `renderHook` +- `src/components/*.test.tsx` for component rendering and user flows +- `src/App.test.tsx` for integration coverage across hooks and UI ## Project Structure + ```text src/ - App.tsx # Thin orchestrator — wires hooks + components + App.tsx # Thin orchestrator for hooks and top-level actions 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.ts # Timeline state, demo mode, replay, import/export useTimeline.test.ts - useConnection.ts # WebSocket lifecycle, Socket.IO handshake, protocol decode, RTT tracking + useConnection.ts # WebSocket lifecycle, Socket.IO handshake, RTT tracking useConnection.test.ts - useSchemaGuard.ts # Schema builder state, parsedSchema memo, Esc key listener + useSchemaGuard.ts # Schema builder state and parsed schema useSchemaGuard.test.ts components/ - ConnectionPanel.tsx # Connection settings form + ConnectionPanel.tsx ConnectionPanel.test.tsx - NamespaceFilter.tsx # Namespace toggle buttons + MessageRow.tsx + MessageRow.test.tsx + NamespaceFilter.tsx NamespaceFilter.test.tsx - TransmitPanel.tsx # Send form with local state - TransmitPanel.test.tsx - SchemaGuardModal.tsx # Schema builder modal + SchemaGuardModal.tsx SchemaGuardModal.test.tsx - MessageRow.tsx # Single message row with expand/copy - MessageRow.test.tsx - TimelinePanel.tsx # Right-panel shell composing MessageRow list + TimelinePanel.tsx TimelinePanel.test.tsx + TransmitPanel.tsx + TransmitPanel.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.ts socketio-utils.test.ts -vitest.setup.ts # @testing-library/jest-dom setup + trace-utils.ts + trace-utils.test.ts +vitest.setup.ts vite.config.ts -index.html package.json -AGENTS.md # Architecture and development guidelines +AGENTS.md ``` +## Verification + +- Type check: `npm run typecheck` +- Build check: `npm run build` +- Test check: `npm run test:run` + ## Notes -- If your endpoint is unavailable, use Demo mode to test the UI behavior. + +- Use Demo mode when the target endpoint is unavailable and you still want to exercise the UI. +- Imported traffic and incoming frames are treated as untrusted input and normalized before use. diff --git a/package-lock.json b/package-lock.json index f5b57f9..e91d677 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "signal-trace", "version": "0.1.0", "dependencies": { + "@azelenets/aegis-design-system": "^0.1.5", "react": "^19.2.4", "react-dom": "^19.2.4" }, @@ -103,6 +104,20 @@ "dev": true, "license": "MIT" }, + "node_modules/@azelenets/aegis-design-system": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@azelenets/aegis-design-system/-/aegis-design-system-0.1.5.tgz", + "integrity": "sha512-Or4uNKVX6Kf490T5phnvYVSXrwltrpOJ61uzEK44IZuIB7UypZ9jH16gEXdgx5ohLsA4u3vpOXcvhB+JPMceUA==", + "license": "MIT", + "dependencies": { + "@material-symbols-svg/react": "^0.1.23", + "leaflet": "^1.9.4" + }, + "peerDependencies": { + "react": "^19.2.0", + "react-dom": "^19.2.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -1259,6 +1274,69 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@material-symbols-svg/react": { + "version": "0.1.23", + "resolved": "https://registry.npmjs.org/@material-symbols-svg/react/-/react-0.1.23.tgz", + "integrity": "sha512-wCHfGI1tflEtradIBLtEuFfd+9PtJIsZFVl+/Xm/h8cS8+cd/2WFFFSigPTCl3ES8kpRYAIyCQppFQDFs8xFFw==", + "license": "Apache-2.0", + "dependencies": { + "@material-symbols/svg-100": "^0.40.2", + "@material-symbols/svg-200": "^0.40.2", + "@material-symbols/svg-300": "^0.40.2", + "@material-symbols/svg-400": "^0.40.2", + "@material-symbols/svg-500": "^0.40.2", + "@material-symbols/svg-600": "^0.40.2", + "@material-symbols/svg-700": "^0.40.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@material-symbols/svg-100": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-100/-/svg-100-0.40.2.tgz", + "integrity": "sha512-x8x/F+4D7FYe9ObdRVuKWy/ErsdUwX5W4J3HB/vLFiGUV2q0OmlKq+w36Dr8cFg0nijfAUqEB/gnLjp8mVScXw==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-200": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-200/-/svg-200-0.40.2.tgz", + "integrity": "sha512-9jAlkUJV6TdTEnRw0kOHZlDlpQYRXz8mdao/v+P9xzmmi3ATMa4QfRJ0oqBIgkqmMHYon2NwSl6PHpMWHBY34A==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-300": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-300/-/svg-300-0.40.2.tgz", + "integrity": "sha512-d7lvHBSAHFaaduIkNCpuQ16dEv+bd5B/Qiizf/NkoOXyHxqDcnesObeh28s4weQcRlg95XfXqzvZSs11UB2wsw==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-400": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-400/-/svg-400-0.40.2.tgz", + "integrity": "sha512-e2yEgZW/OveVT1sGaZW1kkRWTPVghjsJYWy+vIea3q08Fv2o7FCYv23PESMyr5D4AaAXdM5dKWkF1e6yIm4swA==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-500": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-500/-/svg-500-0.40.2.tgz", + "integrity": "sha512-eTNax14JaRlGKpnGTq22c/yQaRBoSOhwHN2yUZmIrmxhz1E1CPydxuRpPxDsUzTjy5GQhtczey/fg480tEMkXQ==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-600": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-600/-/svg-600-0.40.2.tgz", + "integrity": "sha512-S9Db52VquiR8f+KWjWXGXIExZ3GmLi9nH8PPbfoPFsXS9zUXJil/FJ8YRLJJ9lXE3CEzyYIezTQOvhQS7Itdpw==", + "license": "Apache-2.0" + }, + "node_modules/@material-symbols/svg-700": { + "version": "0.40.2", + "resolved": "https://registry.npmjs.org/@material-symbols/svg-700/-/svg-700-0.40.2.tgz", + "integrity": "sha512-jnFwf0t1E+/eAjk9Ssqsy3sc/3af/IDopY2d2yHKOpgEjar2pPFqW2fFG9aztshxKlhPoli765NavZp4w9cWhg==", + "license": "Apache-2.0" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-rc.3", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", @@ -3419,6 +3497,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index 0b18041..ca1f6de 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "signal-trace", + "description": "Signal Trace is a browser-based WebSocket traffic inspector for real-time IoT debugging. It helps you connect to raw WebSocket or Socket.IO endpoints, inspect inbound and outbound frames, validate outgoing JSON against a lightweight schema, and replay or export captured traffic.", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", + "hooks:install": "git config core.hooksPath .githooks", + "typecheck": "tsc -b", "build": "tsc -b && vite build", "preview": "vite preview", "lint": "eslint .", @@ -13,6 +16,7 @@ "test:run": "vitest run" }, "dependencies": { + "@azelenets/aegis-design-system": "^0.1.5", "react": "^19.2.4", "react-dom": "^19.2.4" }, diff --git a/src/App.test.tsx b/src/App.test.tsx index d929842..30fb6df 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -41,6 +41,11 @@ class MockWebSocket { describe('App', () => { const originalWebSocket = globalThis.WebSocket; + const selectProtocolMode = async (user: ReturnType) => { + await user.click(screen.getAllByRole('combobox', { name: 'Protocol Decode' })[0]); + await user.click(screen.getByRole('option', { name: 'SOCKET.IO' })); + }; + beforeEach(() => { MockWebSocket.instances.length = 0; globalThis.WebSocket = MockWebSocket as unknown as typeof WebSocket; @@ -55,10 +60,9 @@ describe('App', () => { const user = userEvent.setup(); render(); - await user.click(screen.getByText('Schema Guard')); await user.click(screen.getByRole('button', { name: 'Open Schema Builder' })); - expect(screen.getByRole('dialog', { name: 'Schema Guard Builder' })).toBeInTheDocument(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); expect(screen.getByText('Schema Guard Builder')).toBeInTheDocument(); }); @@ -67,7 +71,6 @@ describe('App', () => { render(); // Open schema builder and add a required field - await user.click(screen.getByText('Schema Guard')); await user.click(screen.getByRole('button', { name: 'Open Schema Builder' })); const dialog = screen.getByRole('dialog'); await user.click(within(dialog).getByRole('button', { name: 'Add Property' })); @@ -78,6 +81,7 @@ describe('App', () => { // Send a payload that is missing the required field const payloadInput = screen.getAllByLabelText('JSON Payload')[0]; fireEvent.change(payloadInput, { target: { value: '{"action":"ping"}' } }); + await user.click(screen.getByRole('button', { name: 'Demo' })); await user.click(screen.getByRole('button', { name: 'Send Frame' })); expect(screen.getByText('schema_violation')).toBeInTheDocument(); @@ -87,8 +91,9 @@ describe('App', () => { const user = userEvent.setup(); render(); - await user.selectOptions(screen.getAllByLabelText('Protocol Decode')[0], 'socketio'); + await selectProtocolMode(user); await user.click(screen.getAllByLabelText('Socket.IO Handshake')[0]); + await user.click(screen.getByRole('button', { name: 'Connect' })); await user.click(screen.getByRole('button', { name: 'Send Frame' })); expect(screen.getByText('socketio_not_ready')).toBeInTheDocument(); @@ -98,7 +103,7 @@ describe('App', () => { const user = userEvent.setup(); render(); - await user.selectOptions(screen.getAllByLabelText('Protocol Decode')[0], 'socketio'); + await selectProtocolMode(user); await user.click(screen.getAllByLabelText('Socket.IO Handshake')[0]); await user.clear(screen.getAllByLabelText('Socket.IO Namespace')[0]); await user.type(screen.getAllByLabelText('Socket.IO Namespace')[0], '/devices'); @@ -108,14 +113,14 @@ describe('App', () => { expect(ws).toBeDefined(); ws.emitOpen(); - expect(screen.getByText('CONNECTING')).toBeInTheDocument(); + expect(screen.getAllByText('CONNECTING').length).toBeGreaterThan(0); ws.emitMessage('0{"sid":"abc"}'); expect(ws.send).toHaveBeenCalledWith('40/devices'); ws.emitMessage('40/devices'); await waitFor(() => { - expect(screen.getByText('CONNECTED')).toBeInTheDocument(); + expect(screen.getAllByText('CONNECTED').length).toBeGreaterThan(0); }); ws.emitMessage('2'); diff --git a/src/App.tsx b/src/App.tsx index 57680bf..c988d07 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,14 @@ import { useCallback } from 'react'; +import { + Accordion, + Badge, + Container, + Grid, + GridItem, + Navbar, + ThemeProvider, + ThemeToggle, +} from '@azelenets/aegis-design-system'; import { extractCorrelationId, safeJson, @@ -10,7 +20,6 @@ import { useTimeline } from './hooks/useTimeline'; import { useConnection } from './hooks/useConnection'; import { useSchemaGuard } from './hooks/useSchemaGuard'; import { ConnectionPanel } from './components/ConnectionPanel'; -import { NamespaceFilter } from './components/NamespaceFilter'; import { TransmitPanel } from './components/TransmitPanel'; import { SchemaGuardModal } from './components/SchemaGuardModal'; import { TimelinePanel } from './components/TimelinePanel'; @@ -72,10 +81,10 @@ const App = () => { const enrichedPayload = params.autoRefresh && parsedPayload ? { - ...parsedPayload, - ...(Object.hasOwn(parsedPayload, 'id') ? { id: generateMessageId() } : {}), - ...(Object.hasOwn(parsedPayload, 'timestamp') ? { timestamp: now } : {}), - } + ...parsedPayload, + ...(Object.hasOwn(parsedPayload, 'id') ? { id: generateMessageId() } : {}), + ...(Object.hasOwn(parsedPayload, 'timestamp') ? { timestamp: now } : {}), + } : parsedPayload; if (schema.parsedSchema.parseError) { @@ -132,85 +141,101 @@ const App = () => { }, [connection, schema.parsedSchema, timeline]); return ( -
-
-
-
-

R&D // EXPERIMENTAL LAB

-

SIGNALTRACE

-

Browser-based WebSocket traffic inspector for real-time IoT debugging.

-
- -
- - - -
-
+ + + {schema.schemaModalOpen && ( { onUpdateRequired={schema.updateSchemaPropertyRequired} /> )} -
+ ); }; diff --git a/src/components/ConnectionPanel.test.tsx b/src/components/ConnectionPanel.test.tsx index 6cc7d80..c76b2da 100644 --- a/src/components/ConnectionPanel.test.tsx +++ b/src/components/ConnectionPanel.test.tsx @@ -37,7 +37,7 @@ describe('ConnectionPanel', () => { it('shows connection status dot and state label', () => { render(); expect(screen.getByText('CONNECTED')).toBeInTheDocument(); - expect(document.querySelector('.dot.connected')).toBeInTheDocument(); + expect(screen.getByText('CONNECTED').closest('span')).toBeInTheDocument(); }); it('hides Socket.IO fields when handshake is off', () => { diff --git a/src/components/ConnectionPanel.tsx b/src/components/ConnectionPanel.tsx index 9eb52a5..0acb34c 100644 --- a/src/components/ConnectionPanel.tsx +++ b/src/components/ConnectionPanel.tsx @@ -1,4 +1,17 @@ import { memo } from 'react'; +import { + Badge, + Button, + Card, + CardBody, + CardHeader, + FormRow, + FormSection, + Input, + Select, + Textarea, + Toggle, +} from '@azelenets/aegis-design-system'; import type { LinkState } from '../types'; import type { ProtocolMode } from '../lib/trace-utils'; @@ -41,99 +54,105 @@ export const ConnectionPanel = memo(function ConnectionPanel({ onDisconnect, onToggleDemo, }: ConnectionPanelProps) { + const statusVariant = (() => { + switch (connState) { + case 'CONNECTED': + return 'success'; + case 'CONNECTING': + return 'hazard'; + case 'ERROR': + return 'alert'; + default: + return 'ghost'; + } + })(); + return ( -
- - Connection - - - {connState} - - -
- - - - {socketIoHandshake && ( - <> - - -