Skip to content

feat: add txs information on dashboard#3

Merged
mduthey merged 29 commits into
mainfrom
feat/m3-dashboard
May 11, 2026
Merged

feat: add txs information on dashboard#3
mduthey merged 29 commits into
mainfrom
feat/m3-dashboard

Conversation

@mduthey

@mduthey mduthey commented May 11, 2026

Copy link
Copy Markdown
Contributor

No description provided.

mduthey and others added 29 commits May 7, 2026 11:54
Spec at docs/superpowers/specs/2026-05-07-m3-dashboard-design.md.
Plan at docs/superpowers/plans/2026-05-07-m3-dashboard-implementation.md.

v2 architecture: tx3-lift tracker runs as an external sidecar writing
SQLite; the dashboard is a TanStack Start app whose Nitro server functions
read that SQLite directly via Kysely + better-sqlite3. No Rust backend on
the dashboard side. MVP scope: matches list + match detail with parties.
… directly

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous value was the mainnet ticket_policy
(1d9c0b541adc300c19ddc6b9fb63c0bfe32b1508305ba65b8762dc7b). With the
tracker pointed at preview.utxorpc-v0.demeter.run, that filter would
never match anything.

The correct preview policy comes from the TII's preview profile:
4672f28ce36e492e722392359f71e9a9442646f81d856f92d7e163f1.

Plan Task 2 template updated to match.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds createDb in src/lib/db.ts: typed Kysely<DashboardDatabase>
that wraps an existing better-sqlite3 instance (used by tests with
:memory:), or opens tracker.db readonly+fileMustExist with WAL
journaling. Path resolves from opts.path, then TRACKER_DB_PATH env,
then ./tracker.db. Schema types cover matches, cursor, and
_schema_versions rows. Vitest test seeds an in-memory schema and
runs a typed select to prove the type wiring.
- Mark all row-interface fields readonly to match the read-only connection contract.
- Seed _schema_versions in the test SCHEMA_SQL so future tests against that table don't fail at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dialect-agnostic adapter for the subset of `matches.lifted` the
MVP renders. `bytesToHex` re-encodes the tracker's `[u8]`-as-int-array
fields (addresses, hashes) into lowercase hex, `truncateHex` shortens
them with a U+2026 ellipsis, and `parseLifted` shapes the JSON into the
typed `Lifted` record consumed by the matches list and detail views.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… test

- RawLifted.parties is Record<string, unknown>; the inner narrow takes care of each value (was a forced cast).
- Add 9th test for truncateHex when hex.length === edge * 2 exactly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements AP-1 (listMatches, newest-first, limit clamped to [1,200])
and AP-2 (getMatch by hex tx_hash, throws on invalid hex) as typed Kysely
queries returning a consumer-friendly MatchRow shape. Both reuse
parseLifted to populate parties + rawLifted and convert matched_at
(unix seconds) to Date.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t8Array branch

- Replace local RawMatch with Pick<Selectable<MatchesRow>, …> so column types stay tied to the canonical schema in db.ts.
- better-sqlite3 always returns Buffer for BLOB columns; drop the defensive Uint8Array branch and call blob.toString('hex') directly.
- Add a getMatch test covering empty / non-hex / odd-length input.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bare aria-hidden compiles to aria-hidden={true} which React serializes
correctly, but the explicit string form is what a11y linters and the
WAI-ARIA spec examples use. Self-documents intent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the M1 placeholder at `/` with the AP-1 matches list. A
`createServerFn` opens a fresh Kysely DB, runs `listMatches(db, 50)`,
and destroys the connection in a `finally` block. The route's loader
invokes the server fn; the component renders a header, an empty-state
card when there are no rows, and otherwise a table of Tx, Hash (linked
to `/txs/$hash`), Slot, Parties, and When.
…JSON

tx_name is denormalized into both the column and the lifted payload by
the tracker. The column is the indexed/authoritative source; the JSON is
opaque text. If parseLifted ever fails to extract tx_name (malformed or
unexpected shape), the row would render an empty TxNamePill. Reading
from the column eliminates that latent failure mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement AP-2 detail at /txs/$hash:
- Server fn (.inputValidator + .handler) opens createDb, runs getMatch,
  destroys in finally; returns MatchRow | null.
- Loader throws notFound() on null so TanStack Router shows the 404 page.
- Component renders TxNamePill + protocol/profile/slot caption, full hex,
  matched_at (UTC sliced to seconds), back link, parties section
  (chip grid or "No parties annotated" copy), and a <details> accordion
  with pretty-printed raw lifted JSON.

Delete the placeholder /txs index route — the matches list now lives at /.
RouteTree.gen.ts regenerates accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DB invariant says rawLifted is valid JSON (parseLifted has already
parsed it once on the server), but if the wire transport ever truncates
or corrupts it, an unguarded JSON.parse in the render body would crash
the page rather than show the raw text. Tiny try/catch keeps the
pretty-printer for the happy path and falls back to the raw string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop the Overview/Transactions nav (the /txs route was deleted in
Task 9, leaving a dead link). The header now shows brand link, a
"Matches view" caption, and ThemeToggle pushed right via ml-auto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the M1 root README (Rust backend + Axum + utxorpc ingestion) with
the M3 v2 architecture: TanStack Start + external tx3-lift tracker + SQLite
as the integration boundary. Add three docs files under docs/ covering the
two-process topology with C4 diagrams, the AP-1/AP-2 access patterns, and
the operator-facing run guide. Slim frontend/README.md from the TanStack
Start template to a 3-line pointer.

Tracker commit hash in docs/running.md "Tested with" left as a placeholder
for Task 12 (smoke checklist) to fill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- The L1 System node now reads 'Tracker (sidecar) + SSR app …' so the gRPC relation in the L1 diagram is no longer attached to a node a reader would interpret as just the SSR web app.
- 'Production build' section now opens with 'From the repo root:' and uses 'cd dashboard/frontend', matching the first-run section's working directory assumption.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI:
- Run pnpm test --run (was missing from frontend-ci, so unit tests were never gated).
- Add pnpm rebuild better-sqlite3 step so the native binding actually builds on a fresh runner.

Docs:
- docs/running.md empty-state troubleshooting: replace the bogus 'cursor slot' wording with the actual UI message ('No matches yet — confirm the tracker is running.').
- docs/architecture.md: 'reused across requests' was wrong; we open a fresh Kysely per request and destroy in finally. Document that.
- docs/running.md 'Tested with' tracker commit: pin to 04d0b90 (the head of tx3-lang/tx3-lift main at testing time).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
With multi-source tracking, the same tx_name can come from different
protocols (e.g. swap_a_to_b in vyfi vs strike-staking has overlapping
generic verbs). Adding the protocol column at column-1 makes the row
self-explanatory: 'protocol → operation' reads top-to-bottom for the
operator.

Styled as a muted pill so it doesn't compete with the primary-colored
TxNamePill.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ewer

Replace the plain <pre> in 'Raw lifted JSON (debug)' with
react-json-view-lite (~5 KB). Each object/array is clickable to
collapse/expand; primitives get distinct colors. Top-level expanded by
default; nested nodes start collapsed via shouldExpandNode={level < 1}
so the operator gets an overview and drills down where they care.

Falls back to a plain <pre> when JSON.parse fails or yields a non-object,
preserving the resilience of the previous version.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The lib's darkStyles ships with a teal-tinted background that clashes
with the site's deep-dark + pink-accent palette. Replace it with a
custom theme that uses Tailwind palette tokens:

- transparent container (lets the parent details bg-muted/20 show)
- foreground for keys, emerald-400 for strings, amber-400 for numbers/booleans
- muted-foreground for null/undefined/punctuation
- primary (pink) for the expand/collapse chevrons

The custom theme is derived as Partial<typeof darkStyles> rather than
importing the lib's StyleProps interface, which is internal-only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…viewer

Dropping the lib's CSS in the previous commit also dropped the ::before
pseudo-element rules that supplied the ▾/▸ glyphs. The icon spans were
rendered with reserved width but no content, so expandable nodes looked
indistinguishable from primitives.

Tailwind 4 generates the content via before:content-['…']; same effect,
no lib CSS dependency. Also adds an ellipsis hint to the collapsed-content
state so collapsed objects show '…' instead of an empty span.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bold

w-3/text-xs (inherited) was tight. Goes to w-4 + text-base + font-bold
+ leading-none so the ▾/▸ glyphs render visibly larger without breaking
the row's vertical rhythm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… detail header

The dashboard is going back to single-protocol focus (Indigo for now;
multi-protocol moves to a tabbed UX later, not a mixed list), so the
protocol column adds noise without value. Reverts to the M3 spec's
five-column shape (Tx · Hash · Slot · Parties · When).

Detail header gets a 'View on cexplorer ↗' link in the meta row, next
to '← back to list'. The URL is derived from the match's profile_name
so mainnet / preview / preprod each go to the right subdomain. The
hash itself stays as plain selectable text — better for copy/paste
than turning it into a link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps each <td>'s content with a TanStack Router <Link>, so any click
in the row navigates to /txs/$hash. The padding moves from <td> to
<Link className='block …'> so the link fills the cell — no dead space.

Why per-cell instead of <tr onClick>: preserves middle-click (open in
new tab), Cmd+click, right-click context menu, and keyboard navigation
(Tab + Enter), all of which an onClick handler would silently break.

Adds 'group cursor-pointer hover:bg-muted/30' on <tr> for row-level
hover feedback, and 'group-hover:underline' on the hash text so the
key affordance still gets emphasis on row hover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@mduthey mduthey merged commit 9bef550 into main May 11, 2026
1 check passed
@mduthey mduthey deleted the feat/m3-dashboard branch May 11, 2026 14:45
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.

1 participant