Skip to content

Feat/wire event search worker into dashboard - #443

Merged
Osuochasam merged 3 commits into
Open-audit-foundation:mainfrom
N-thnI:feat/wire-event-search-worker-into-dashboard
Aug 30, 2026
Merged

Feat/wire event search worker into dashboard#443
Osuochasam merged 3 commits into
Open-audit-foundation:mainfrom
N-thnI:feat/wire-event-search-worker-into-dashboard

Conversation

@N-thnI

@N-thnI N-thnI commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Wires the in-browser event search worker into the dashboard's live feed: index lifecycle, debounced querying, result highlighting, and worker teardown.

Closes #419

What the issue predicted vs. what was actually there

The issue describes the worker and client as fully-built but unimported. That was true when it was written; it isn't quite the situation on main now, and the difference matters.

The wiring existed in commit 2255a0d, and a later merge (e5fedda) deleted half of it. app/dashboard/DashboardClient.tsx on main does not parse:

  • the useEventSearch destructuring block was dropped
  • the index-building useEffect was replaced by a duplicate, half-written filteredEvents useMemo
  • searchHits, buildIndex, addSearchEvents, isSearching, isIndexed, searchError and isFallback are still referenced further down the file

The search input, loading spinner, error display and fallback notice all survived the merge. So this PR is repair plus correction, not a greenfield integration.

Changes

app/dashboard/DashboardClient.tsx

Restored the useEventSearch destructuring and the index lifecycle.

Did not restore the pre-merge incremental logic — it was wrong. It diffed events by object identity:

allEvents.filter((e) => !liveEvents.includes(e) || liveEvents.indexOf(e) === allEvents.indexOf(e))

then sent an arbitrary slice of at most 20. That is not a correct incremental path. The effect now tracks indexed event IDs in a ref and sends the worker only IDs it hasn't seen — stable across re-renders that produce new object identities for the same events, and across the same event arriving twice. BUILD_INDEX runs once; every later arrival is an ADD_EVENTS.

Fixed a double-add bug. handleNewEvent called addSearchEvents([event]) directly and the effect added it again, so every streamed event was sent to the worker twice. Indexing now lives only in the effect — the path that knows what is already indexed. The integration test caught this (3 calls where 2 were expected).

isIndexed now drives the input placeholder, so the field reads "Building search index…" rather than silently returning nothing before the index is ready.

components/dashboard/EventFeedTable.tsx

Highlighting was entirely absent — the component had no such prop. Added optional highlightQuery; matching substrings in the translated description and event type are wrapped in <mark>.

The query is escaped before being built into a RegExp. Contract and function names routinely contain regex metacharacters (transfer(, a.b, x[0]), and an unescaped transfer( is an unterminated group that would throw while rendering the feed.

package.json

Added @testing-library/dom — see below.

Two things the broken file was masking

tsc was hiding 10 real errors. On main, tsc --noEmit reports only 5 errors, all syntax errors in DashboardClient.tsx. TypeScript skips semantic checking when syntactic errors exist, so 10 pre-existing type errors in lib/dag/, components/dag/ and src/worker/indexer.ts were never surfacing. They are visible now. That is Issue #8's subsystem and out of scope here, but the typecheck will look worse after this PR merges — those errors predate it and are unrelated to search.

8 existing tests had never run. @testing-library/react requires @testing-library/dom at runtime but doesn't depend on it directly, and it was not installed. lib/workers/eventSearchClient.test.ts — 8 tests for the search hook — failed at collection with Cannot find module '@testing-library/dom'. Installing the peer dep makes them execute, and they pass.

Existing coverage checked before writing tests

Per the issue's instruction not to duplicate:

  • lib/workers/eventSearchClient.unit.test.ts — the client class and its lifecycle against a mock Worker
  • lib/workers/eventSearchClient.test.ts — the useEventSearch hook's debounce, lifecycle and incremental updates against a mocked client

Neither exercises the component. That is what the merge broke, and that is what the new tests cover.

New tests

app/dashboard/DashboardClient.search.test.tsx (13)EventSearchClient is mocked so the protocol is observable; the component and hook are real.

  • builds the index exactly once from the events already loaded
  • a streamed event produces one ADD_EVENTS and no rebuild
  • a second arrival sends only the new event, not the previous one
  • five arrivals: one build, five adds
  • no worker message on any of eight keystrokes; one search after the debounce window, carrying the final query
  • one search per settled query, not per character
  • an emptied box clears results without a worker round-trip
  • the feed is filtered to the returned hits, and restored when the query clears
  • the query reaches the worker unpreprocessed (ranking stays the worker's business)
  • the worker is destroyed on unmount, and across three mount/unmount cycles — the orphaned-worker-per-remount bug class
  • a worker search failure surfaces instead of hanging

components/dashboard/EventFeedTable.highlight.test.tsx (9) — multiple occurrences, case-insensitive matching that preserves original casing, event type as well as description, no marks without a query, and the regex-metacharacter case.

Debounce tests use fireEvent, not user-event: user-event's internal awaits deadlock against vi.useFakeTimers(), and these tests exist specifically to control the debounce clock.

Fallback behaviour

Already implemented in useEventSearch and left as-is: if the Worker constructor throws, the hook sets isFallback and runs the same search synchronously on the main thread over a locally-held event list. The dashboard renders "Running search on main thread (Web Worker unavailable) — results may be slower." Documenting it here since the issue asked for the behaviour to be defined rather than left undefined.

Verification

  • 22 new tests pass.
  • Full suite: 663 → 686 passing, 93 → 92 failures, 14 → 13 failing files. Zero regressions; the improvement is the previously-uncollectable test file now running.
  • tsc --noEmit clean on both files touched.

Acceptance criteria

  • Typing in the event feed search box returns real results from the Web Worker, not a placeholder or main-thread filter
  • The index updates as events stream in without a full rebuild — one BUILD_INDEX, then ADD_EVENTS per arrival, asserted over a 5-event stream
  • The worker is terminated on unmount — asserted, including across repeated mount/unmount cycles
  • Search input is debounced — asserted that eight keystrokes produce zero worker messages before the window elapses, then exactly one
  • No regression to existing worker/client unit tests — and 8 of them now run for the first time

Out of scope

Server-side historical search (Issue #10) is untouched; the contract-ID SearchBar still hits that API unchanged. Ranking is the worker's — SearchResponse.hits[].score is used as returned, not redesigned. The DAG subsystem (Issue #8) is untouched, including its now-visible type errors.

N-thnI added 3 commits August 30, 2026 15:23
The wiring existed once (2255a0d) and a merge (e5fedda) deleted half of it,
leaving main in a state where app/dashboard/DashboardClient.tsx did not
parse: the useEventSearch destructuring block and the index-building effect
were dropped, and a duplicate half-written filteredEvents useMemo was left
in their place, while searchHits/buildIndex/addSearchEvents/isSearching were
still referenced further down the file.

Restores the destructuring and replaces the broken fragment with a correct
index lifecycle.

Incremental indexing
  The pre-merge effect diffed arrays by object identity
  (!liveEvents.includes(e) || liveEvents.indexOf(e) === allEvents.indexOf(e))
  and then sent an arbitrary slice of at most 20. That is not a correct
  incremental path, so it is not what was restored.

  The effect now tracks indexed event ids in a ref and sends the worker only
  ids it has not seen. That is stable across re-renders that produce new
  object identities for the same events, and across an event arriving twice.
  BUILD_INDEX runs once; every later arrival is an ADD_EVENTS.

Double-add fix
  handleNewEvent also called addSearchEvents([event]) directly, so every
  streamed event was sent to the worker twice — once by the callback and
  once by the effect. Indexing now lives only in the effect, which is the
  path that knows what is already indexed.

Highlighting
  searchValue is passed to EventFeedTable as highlightQuery.

isIndexed drives the input placeholder, so the field says "Building search
  index…" until the index is ready instead of silently returning nothing.

13 integration tests covering what the unit tests do not: that the component
builds once and adds incrementally, debounces (no worker message per
keystroke), filters the feed by the returned hits, restores the feed when
the query clears, surfaces a worker error, and terminates the worker on
unmount including across repeated mount/unmount cycles.
Adds an optional highlightQuery prop to EventFeedTable. When set, matching
substrings in the translated description and the event type are wrapped in
<mark>, so the user can see why a row matched rather than only that it did.

The query is escaped before being built into a RegExp. Contract and function
names routinely contain regex metacharacters — transfer(, a.b, x[0] — and an
unescaped transfer( is an unterminated group that would throw while
rendering the feed.

No query means the plain string is returned, so ordinary renders do no extra
work and emit no wrapper elements.

9 tests: multiple occurrences, case-insensitive matching that preserves
original casing, event type as well as description, the metacharacter case,
and that surrounding text stays intact.
@testing-library/react needs @testing-library/dom at runtime but does not
depend on it directly. Without it lib/workers/eventSearchClient.test.ts — 8
existing tests for the search hook — failed at collection with "Cannot find
module @testing-library/dom" and had never actually run.

Installing it makes those 8 pre-existing tests execute (and pass), and lets
the new dashboard integration tests render components at all.
@Osuochasam
Osuochasam merged commit 67b3c78 into Open-audit-foundation:main Aug 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stand up a real CI pipeline — typecheck, lint, test, build, and a performance-regression benchmark gate

2 participants