Skip to content

Add dbviewer plugin: database viewer devtool from the design handoff - #8942

Open
pubkey wants to merge 9 commits into
masterfrom
claude/db-viewer-design-rxdb-oonmx9
Open

Add dbviewer plugin: database viewer devtool from the design handoff#8942
pubkey wants to merge 9 commits into
masterfrom
claude/db-viewer-design-rxdb-oonmx9

Conversation

@pubkey

@pubkey pubkey commented Aug 10, 2026

Copy link
Copy Markdown
Owner

This PR contains:

  • A NEW FEATURE: the dbviewer plugin, a database viewer devtool implemented from the static design canvas handoff (RxDB brand dark theme, zero border radii, self-contained, no framework and no asset files)
  • IMPROVED TESTS: unit tests for the viewer helpers (schema analysis, diff, WILL RUN codegen, selector parsing/matching) and the dump data source
  • IMPROVED DOCS: new dbviewer.md reference page with in-text links from dev-mode.md, logger.md and rx-database.md

Describe the problem you have without this PR

There is no built-in way to inspect and edit the data of a running RxDB database. Developers fall back to storage-specific browser devtools (IndexedDB tab, SQLite shells) which know nothing about RxDB revisions, replication, tombstones or the query planner, and which cannot open a db.exportJSON() dump from a device you cannot reach.

This PR implements the database viewer design into the repo as src/plugins/dbviewer. mountRxDBViewer({ database, parent }) renders the full devtool into any DOM element, and RxDBDbViewerPlugin adds a db.launchDbViewer() convenience method. All screens run on real data:

  • Global chrome: top bar with db / storage / version identity, ⌘K command palette, Refresh; left rail with live collection counts, replication status glyphs ( running, idle, error, stopped) and the tools
  • Table view: paginated grid (100 rows per page), per-page sorting, multi-select, inline cell editing, and the document drawer with FIELDS / INTERNALS / ATTACHMENTS (inline image previews) and the WILL RUN block that previews the exact upsert() call with changed lines highlighted before Apply
  • JSON view with syntax colors; Observe mode re-runs the query live and highlights freshly updated documents
  • Query bar: Mango selector input with history and favourites (⌘S), syntax errors with a caret at the exact character position while previous results stay visible, plus the no-matches / empty-collection / empty-database states
  • Destructive confirmation: type-the-collection-name modal that states the blast radius, replication consequences and tombstones
  • Live activity map: app → collections → remote columns drawn from the real change stream, replication feeds and query-cache counters; glyph particles (+/~/- writes, ?/ queries, / replication), 60s sparklines, per-node write pulses rate-limited to 4 Hz, idle/nothing-yet states, live-queries sub-panel and the last-60s summary bar. No document contents are drawn on the map
  • Schema panel: sampled per-field type-share bars, presence percentages, value details and violations against the declared schema
  • Query lab: index used, examined/returned/elapsed cards and an execution plan derived from the query planner (queryPlan), with findings for unindexed selector fields and $regex full scans
  • Replication panel: per-state pull/push status, last error, and a pausable live feed of replicated documents with direction glyphs and byte sizes
  • Changes panel: pausable, filterable master/detail write feed with unified diffs computed from previousDocumentData
  • Storage panel: engine, per-collection documents, tombstones (counted through the storage instances) and attachment bytes, plus a cleanup action
  • Dump mode: the same UI over a db.exportJSON() export, read-only with a persistent banner; live-only features are disabled with "not available on a dump" hints
  • Phone layout: below 640px the UI switches to the stacked read-only screens (collections → document list → document view) with 44px touch rows

Deliberately out of scope for this PR (documented in dbviewer.md): WebRTC remote pairing to a database on another device (the design's connection screens), the TanStack Devtools surface, and cross-tab instance listing. The data-source abstraction and the surface-independent chrome are built so these can be layered on later.

All screens were verified against the design canvas by mounting the built plugin over a memory-storage database with Playwright.

Error codes DVW1/DVW2 were added to rx-error/dev-mode messages, and the plugin is wired into the package.json exports and the plugins build.

Todos

  • Tests
  • Documentation
  • Typings
  • Changelog

🤖 Generated with Claude Code

https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ


Generated by Claude Code

Implements the database viewer design handoff as a new plugin under
src/plugins/dbviewer. The viewer mounts into any DOM element in plain
TypeScript without a framework dependency and works over a live
RxDatabase or a static exportJSON() dump (read-only).

Screens implemented from the design:
- Global chrome: top bar with database identity, command palette,
  left rail with live collection counts, replication states and tools
- Table view with pagination, sorting, multi-select, inline editing,
  and the document drawer with fields, internals, attachments and the
  WILL RUN preview of the exact upsert call before applying edits
- JSON view with syntax colors and live update highlighting
- Mango query bar with history/favourites, exact-position syntax
  errors and the no-matches / empty-collection / empty-database states
- Destructive delete confirmation with type-to-confirm blast radius
- Live activity map: app/collections/remote columns, event particles,
  60s sparklines, live query sub-panel, summary counters
- Schema panel with sampled type shares, presence and violations
- Query lab with query-planner based execution plan and findings
- Replication panel with per-state status and live document feed
- Changes panel with filterable write feed and unified diffs
- Storage panel with tombstone counts, attachment bytes and cleanup
- Narrow-viewport read-only phone layout below 640px

Adds error codes DVW1/DVW2, unit tests for the pure helpers and the
dump data source, a docs page and a changelog entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

⚠️ Verify Test Reproduction: Tests PASSED without the fix (unexpected)

The changed tests do not fail without the source changes from this PR. Please inspect whether the test changes actually test the bug that the source changes fix.

This workflow runs the changed tests without the source fix to verify they reproduce the bug.

Show output
...(truncated, showing last 200 of 4293 lines)
      �[32m✓ �[39mshould compress text but not jpeg on the same document
    isCompressibleType()
      �[32m✓ �[39mshould match wildcard patterns
      �[32m✓ �[39mshould match exact patterns
      �[32m✓ �[39mshould be case-insensitive
      �[32m✓ �[39mshould NOT match non-compressible types
      �[32m✓ �[39mshould match types that include RFC 2045 parameters like charset
    MIME type preservation
      �[32m✓ �[39mfull roundtrip through storage should preserve MIME type on getData()
      �[32m✓ �[39mfull roundtrip should preserve MIME type for non-compressible type

  attachments-compression.test.ts (mode: gzip)
    basics
      �[32m✓ �[39mcompress->decompress
    CRUD
      �[32m✓ �[39mshould insert one attachment
      �[32m✓ �[39mshould get the attachment
      �[32m✓ �[39mshould get the data
    compare size
      �[32m✓ �[39mshould have a smaller size when compression is used
    selective compression
      �[32m✓ �[39mshould compress a compressible type (text/plain) and roundtrip correctly
      �[32m✓ �[39mshould NOT compress a non-compressible type (image/jpeg) but still roundtrip correctly
      �[32m✓ �[39mshould compress text but not jpeg on the same document
    isCompressibleType()
      �[32m✓ �[39mshould match wildcard patterns
      �[32m✓ �[39mshould match exact patterns
      �[32m✓ �[39mshould be case-insensitive
      �[32m✓ �[39mshould NOT match non-compressible types
      �[32m✓ �[39mshould match types that include RFC 2045 parameters like charset
    MIME type preservation
      �[32m✓ �[39mfull roundtrip through storage should preserve MIME type on getData()
      �[32m✓ �[39mfull roundtrip should preserve MIME type for non-compressible type
WARN LOG: �[36m'-------------- RxDB Open Core RxStorage -------------------------------
You are using the free Dexie.js based RxStorage implementation from RxDB https://rxdb.info/rx-storage-dexie.html?console=dexie 
While this is a great option, we want to let you know that there are faster storage solutions available in our premium plugins.
For professional users and production environments, we highly recommend considering these premium options to enhance performance and reliability.
 https://rxdb.info/premium/?console=dexie 
If you already purchased premium access you can disable this log by calling the setPremiumFlag() function from rxdb-premium/plugins/shared.
---------------------------------------------------------------------'�[39m

  migration-storage.test.ts (prev-major to newest (dexie))
    basic migrations
      �[32m✓ �[39mcreate both databases
      �[32m✓ �[39mshould migrate all documents
      �[32m✓ �[39mshould migrate in parallel
      �[32m✓ �[39mmigrate new->new should also work
    issues
      �[32m✓ �[39mmigration with multiple collections

  migration-storage.test.ts (newest to newest)
    basic migrations
      �[32m✓ �[39mcreate both databases
      �[32m✓ �[39mshould migrate all documents
      �[32m✓ �[39mshould migrate in parallel
      �[32m✓ �[39mmigrate new->new should also work
    issues
      �[32m✓ �[39mmigration with multiple collections

  webmcp.test.ts
    �[32m✓ �[39mshould register query tool when registerWebMCP is called
    �[32m✓ �[39mshould wait for changes using wait_changes tool
    �[32m✓ �[39mchanges tool should return documents without internal meta fields
    �[32m✓ �[39mshould iterate over changes using checkpoint
    �[32m✓ �[39mshould execute modifier tools successfully (insert/upsert/delete)
    �[32m✓ �[39mshould unregister tools when collection is closed
    �[32m✓ �[39mshould not register modifier tools when readOnly is true
    �[32m✓ �[39mshould emit log$ and error$ events for executed tools
    �[32m✓ �[39mshould register tools for newly added collections dynamically
    custom targets
      �[32m✓ �[39mshould build the same tools for a target that is not an RxCollection
      �[32m✓ �[39mshould throw WMCP1 from a target that has no such document
      �[32m✓ �[39mshould not build modifier tools for a readOnly target
      �[32m✓ �[39mshould respect awaitReplicationsInSync on a target
      �[32m✓ �[39mshould register at a given modelContext and unregister on close
      �[32m✓ �[39mshould emit log$ and error$ for a target
      �[32m✓ �[39mshould build a working target from an RxCollection

  crdt.test.ts
    collection creation
      �[32m✓ �[39mshould throw if the wrong conflict handler is set
      �[32m✓ �[39mshould automatically set the CRDT conflict handler
    .insert()
      �[32m✓ �[39mshould insert a document and initialize the crdt state
      �[32m✓ �[39mshould insert document via bulkInsert
    .insertCRDT()
      �[32m✓ �[39mshould insert the document
      �[32m✓ �[39mshould insert the document with undefined argument
      �[32m✓ �[39mshould respect the if-else logic
    .remove()
      �[32m✓ �[39mshould delete the document via .remove
    .incrementalPatch()
      �[32m✓ �[39mshould update the document
    disallowed methods
      �[32m✓ �[39mshould throw on incrementalModify
      �[32m✓ �[39mshould throw on modify
    redirected methods
      �[32m✓ �[39mshould redirect patch through updateCRDT
      �[32m✓ �[39mshould redirect incrementalRemove through updateCRDT
      �[32m✓ �[39mshould redirect update through updateCRDT
    conflict handling
      init
        �[32m✓ �[39minit
      .getCRDTConflictHandler()
        �[32m✓ �[39mshould merge 2 inserts correctly
        �[32m✓ �[39mshould preserve schema default values during conflict resolution
        �[32m✓ �[39mshould preserve the composite primary key during conflict resolution
      conflicts during replication
        �[32m✓ �[39mshould merge the +1 increments

  population.test.js
    createRxSchema
      positive
        �[32m✓ �[39mshould allow to create a schema with a relation
        �[32m✓ �[39mshould allow primary as relation key
        �[32m✓ �[39mshould allow to create a schema with a relation in nested
        �[32m✓ �[39mshould allow to create relation of array
        �[32m✓ �[39mshould allow to create relation with nullable string
      negative
        �[32m✓ �[39mthrow if ref-type is no string
        �[32m✓ �[39mthrow if ref-type is no string (array)
    RxDocument().populate()
      positive
        �[32m✓ �[39mpopulate top-level-field
        �[32m✓ �[39mpopulate nested field
        �[32m✓ �[39mpopulate string-array
        �[32m✓ �[39mpopulate with primary as ref
      negative
        �[32m✓ �[39mthrow DOC5 for a path that does not exist in the schema, even when the value is falsy
        �[32m✓ �[39mthrow DOC6 when populating a non-ref schema field, even when the value is falsy
    RxDocument populate via pseudo-proxy
      positive
        �[32m✓ �[39mpopulate top-level-field
        �[32m✓ �[39mpopulate nested field
    issues
      �[32m✓ �[39m#222 population not working when multiInstance: false
      �[32m✓ �[39mpopulate array should preserve the order of ref ids when two documents reference the same set in different order
      �[32m✓ �[39mpopulate array when ref is defined on items instead of on the array field

  leader-election.test.js
    .die()
      �[32m✓ �[39mother instance applies on death of leader
    election
      �[32m✓ �[39ma single instance should always elect itself as leader
      �[32m✓ �[39mshould not elect as leader if other instance is leader
      �[32m✓ �[39mwhen 2 instances apply at the same time, one should win
      �[32m✓ �[39mwhen many instances apply, one should win
      �[32m✓ �[39mwhen the leader dies, a new one should be elected
    cleanup
      �[32m✓ �[39mshould properly call die() on the elector when the database is closed
      �[32m✓ �[39m#8893 close() must not resolve before the broadcast channel is closed
    integration
      �[32m✓ �[39mnon-multiInstance should always be leader
      �[32m✓ �[39mnon-multiInstance: waitForLeadership should instant
      �[32m✓ �[39mwaitForLeadership: run once when instance becomes leader

  import-export.test.js
    Collection
      .exportJSON()
        �[32m✓ �[39mexport the collection
        �[32m✓ �[39mexport encrypted as decrypted
      .importJSON()
        positive
          �[32m✓ �[39mimport json
        negative
          �[32m✓ �[39mshould not import if schema is different
    Database
      .exportJSON()
        �[32m✓ �[39mshould export a valid dump
        �[32m✓ �[39mexport encrypted as decrypted
        �[32m✓ �[39mexport with multiple collections
        �[32m✓ �[39mexport 1 of 2 collections
      .importJSON()
        positive
          �[32m✓ �[39mimport dump
        negative
          �[32m✓ �[39mshould not import if schema is different
    issues
      �[32m✓ �[39m#319 collections must be created before importDump
      �[32m✓ �[39m#1396 import/export should work with attachments

  database-lifecycle.ts
    �[32m✓ �[39mdo some writes updates and deletes and cleanups and reopens

  last.test.ts (dexie)
    �[32m✓ �[39mrun a minimal performance test to ensure the performance function works
    �[32m✓ �[39mensure all Memory RxStorage instances are closed
    �[32m✓ �[39mensure every db is cleaned up
    �[32m✓ �[39mensure all collections are closed
    �[32m✓ �[39mensure all BroadcastChannels are closed
    �[32m✓ �[39mensure all replication states are closed
    �[32m✓ �[39mensure all RemoteMessageChannels have been closed
    �[32m✓ �[39mensure all websockets have been closed
    �[32m✓ �[39mensure all leader electors are dead
    �[32m✓ �[39mexit the process

Chrome Headless 151.0.0.0 (Linux 0.0.0): Executed 1354 of 1354�[32m SUCCESS�[39m (53.173 secs / 52.408 secs)
�[32mTOTAL: 1354 SUCCESS�[39m



View full workflow run

pubkey commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

The "tests passed without the fix" warning is expected here: this PR adds a new feature (the dbviewer plugin), not a bugfix, so there is no pre-existing failing behavior for the new tests to reproduce. The new test/unit/dbviewer.test.ts covers the new plugin's helpers and dump data source, which cannot even be imported without the source changes in this PR (the browser suite shown in the bot output does not include that node-only test file).

The test-code-style failure on the first commit was codespell flagging the intentionally invalid JSON literal fals in a testcase; fixed in f3ccd5b by using a different invalid token.


Generated by Claude Code

claude added 7 commits August 10, 2026 15:21
A round toggle button in the bottom right corner docks the dbviewer
devtool to the bottom of the page. The viewer is mounted lazily on
first open and outside of the angular zone, so server side rendering
and change detection are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
… fix the missing logo

- On load the browser console logs a direct db-viewer url; opening the
  app with #dbviewer in the url opens the viewer directly. The open
  state is a signal so the binding also updates without zone.js
  change detection.
- The round nub is now a labeled "Open DB Viewer" button.
- FIX postinstall:logo copied from the outdated path
  docs-src/files/logo/logo.svg, which led to a missing
  /assets/logo.svg in the browser. It now copies from
  docs-src/static/files/logo/logo.svg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
- Observe mode is on by default on a live database, results update
  as documents change without toggling first.
- The Schema panel got an Analysis / JSON schema segmented toggle
  that shows the declared schema with syntax colors.
- The query bar and Query lab accept relaxed JavaScript object
  syntax like { name: 'foo' }: unquoted keys, single quotes and
  trailing commas are converted to strict JSON by a character
  walker that leaves string contents untouched. Added testcases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
- The top bar and phone header now render the official RxDB logo
  as inline svg instead of a css gradient circle. The svg is
  embedded as a constant, so the viewer stays self-contained.
- Live map rows no longer stretch over the panel height and long
  replication errors on a remote node are truncated with the full
  text in the tooltip, so the app, collection and remote nodes of
  a row stay on the same line.
- The angular example database is named my-angular-db instead
  of db, so the viewer top bar shows a recognizable name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
…he viewer

- New showCloseButton option renders a close icon in the top bar.
  Clicking it emits on the close$ observable of the handle, so an
  embedding host decides what closing means. The angular example
  subscribes to it and hides its bottom dock instead of rendering
  its own close button.
- The Live map no longer draws pulled documents on the
  app to collection lane. The event hub reads eventBulks$ and marks
  writes with the replication-downstream context as fromReplication,
  so a pulled document is one particle on the remote lane only. The
  app node write rate and the writes counter also exclude them, and
  the Changes detail names the source. Added a testcase.
- Errors are shown inside the viewer instead of console or alert:
  a failed cleanup renders a danger box with the plugin hint (the
  plugin-missing stub throws synchronously, which previously
  escaped to the console), invalid dump files show an inline
  message in Settings and a failed delete shows in the modal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
…eed collection names

- Errors are shown as a popup instead of inline, so the drawer
  buttons stay reachable for a retry. RxError parameters are
  serialized into a readable JSON view (circular-safe, capped)
  with a copy button and a link to the error code docs.
- Editing an existing document runs incrementalPatch() with only
  the changed fields instead of upsert(), so preInsert hooks like
  the color-uniqueness check of the angular example no longer
  fire for updates. The WILL RUN block previews the exact
  findOne().incrementalPatch() call. Added testcases.
- All JSON views (results, JSON schema, error parameters) have a
  copy-to-clipboard button with the same icon the docs use.
- Removed the commands button and the command palette.
- The help button opens https://rxdb.info/overview.html
- FIX The change feed showed ? instead of the collection name and
  the Live map dropped write particles, because the events of
  eventBulks$ do not carry the collectionName. It is now taken
  from the bulk. Added a testcase.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
Labels like "no write this session" were dim gray at 9px and
additionally faded by the idle-row opacity, which made them
unreadable. Informational micro-copy now uses the muted
foreground at 10px, and idle rows fade only their track lines
and node borders, never their text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XSWnG8xBeMGgwaLKniRnyJ
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.

2 participants