Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
48dcdaf
feat(example-apps): add DashRate review-rating example app
thephez Jun 23, 2026
3bf7861
style(dashrate): refine review UI copy and layout
thephez Jun 23, 2026
c096c22
feat(dashrate): set a default testnet review contract
thephez Jun 23, 2026
78b963b
refactor(dashrate): partial-fill stars and divider-based review layout
thephez Jun 24, 2026
fc1c960
feat(dashrate): rating distribution histogram and filter-by-rating
thephez Jun 24, 2026
0adbc4c
feat(dashrate): show DPNS names for reviewers
thephez Jun 24, 2026
413b79b
fix(dashrate): clear stale reviews on switch and show a loading state
thephez Jun 24, 2026
1d85517
chore: update comments, run format, update readme
thephez Jun 24, 2026
eb20c86
test(dashrate): expand coverage and add coverage tooling
thephez Jun 24, 2026
1c1be48
chore: update gitignore
thephez Jun 24, 2026
7f84659
feat(dashrate): improve the sign-in form
thephez Jun 24, 2026
8526c7c
fix(dashrate): refresh recent reviews after saving a review
thephez Jun 24, 2026
c81ae39
ci(dashrate): run test and build on source changes
thephez Jun 24, 2026
59f652a
refactor(dashrate): extract App into components, hooks, and lib modules
thephez Jun 25, 2026
7e74c33
feat(dashrate): polish the review composer and history
thephez Jun 25, 2026
cca77fb
chore(dashrate): trim console noise and add small-phone breakpoints
thephez Jun 25, 2026
dc12d59
feat(dashrate): sign in on Enter in the mnemonic field
thephez Jun 25, 2026
2bf9824
docs(dashrate): align CLAUDE.md architecture with components/hooks la…
thephez Jun 25, 2026
57c2c3a
feat(dashrate): scroll to detail and pin the header on mobile
thephez Jun 25, 2026
367fd29
feat(dashrate): polish the rating histogram and flatten the sidebar
thephez Jun 25, 2026
18c753c
fix(dashrate): correct mobile scroll offset and touch-hover on the hi…
thephez Jun 25, 2026
dd1d384
feat(dashrate): add a View on GitHub footer link
thephez Jun 25, 2026
1c5d129
test(dashrate): add component, hook, and Playwright e2e suites
thephez Jun 25, 2026
f82021e
test(dashrate): cover App orchestration and errorMessage shapes
thephez Jun 25, 2026
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
53 changes: 53 additions & 0 deletions .github/workflows/dashrate-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: DashRate CI

on:
push:
branches: [main]
paths:
- 'example-apps/dashrate/**'
- '.github/workflows/dashrate-ci.yml'
pull_request:
branches: [main]
paths:
- 'example-apps/dashrate/**'
- '.github/workflows/dashrate-ci.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: dashrate-ci-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: test + build
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: example-apps/dashrate
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .nvmrc
cache: npm
cache-dependency-path: example-apps/dashrate/package-lock.json

- name: Show Node/npm versions
run: |
node -v
npm -v

- name: Install
run: npm ci

- name: Test
run: npm test

# tsc -b runs as part of build, so this covers the typecheck too.
- name: Build
run: npm run build
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ node_modules/
example-apps/*/node_modules/
example-apps/*/dist/
example-apps/*/dist-ssr/
example-apps/*/coverage/
example-apps/*/*.local

# Example apps (Playwright artifacts)
example-apps/*/playwright-report/
example-apps/*/test-results/
1 change: 1 addition & 0 deletions example-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Stand-alone applications built on top of the same `@dashevo/evo-sdk` used by the
- [dashmint-lab/](./dashmint-lab/) — React + TypeScript + Vite SPA for minting, viewing, transferring, and trading NFT-style collectible cards on Dash Platform testnet. Shares the browser-safe SDK core (`setupDashClient-core.mjs`) with the Node tutorials at the repo root.
- [dashproof-lab/](./dashproof-lab/) — React + TypeScript + Vite proof-of-existence tutorial app that hashes files locally in the browser, anchors SHA-256 proofs on Dash Platform testnet, verifies files by hash, and reviews proof history by owner or chain ID. Also uses the shared browser-safe SDK core from the parent repo.
- [dashnote/](./dashnote/) — React + TypeScript + Vite notes app for Dash Platform testnet. Create, edit, and delete notes against a small `note` data contract; supports a "Remember Me" read-only browse mode, optimistic localStorage cache, and ships a single-file zero-build read-only companion at `dashnote-lite.html`. Also uses the shared browser-safe SDK core from the parent repo.
- [dashrate/](./dashrate/) — React + TypeScript + Vite app for rating Platform tutorial resources on Dash Platform testnet, built to showcase Platform 4.0's relational document queries: provable `count`, grouped `count` (`GROUP BY`) for the per-star distribution, range counts, and `where` filtering. One review per identity per resource (editable, with document history); read-only browsing works without signing in. Also uses the shared browser-safe SDK core from the parent repo.
3 changes: 3 additions & 0 deletions example-apps/dashrate/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist
node_modules
coverage
4 changes: 4 additions & 0 deletions example-apps/dashrate/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"printWidth": 80,
"trailingComma": "all"
}
Comment on lines +1 to +4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Enable single quotes in the app-local Prettier config.

npm run format in this workspace will currently keep emitting double-quoted JS/TS output, which breaks the repo formatting contract for example-apps/dashrate.

♻️ Proposed fix
 {
   "printWidth": 80,
+  "singleQuote": true,
   "trailingComma": "all"
 }

As per coding guidelines, **/*.{js,mjs,ts,tsx,json}: Prettier formatting must use single quotes, 2-space indentation, and trailing commas.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"printWidth": 80,
"trailingComma": "all"
}
{
"printWidth": 80,
"singleQuote": true,
"trailingComma": "all"
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@example-apps/dashrate/.prettierrc.json` around lines 1 - 4, The app-local
Prettier config is missing the single-quote setting, so formatting in this
workspace still emits double-quoted JS/TS output. Update the Prettier
configuration in the dashrate app’s .prettierrc.json to enable single quotes
alongside the existing printWidth and trailingComma settings, so npm run format
follows the repo’s formatting contract.

Source: Coding guidelines

80 changes: 80 additions & 0 deletions example-apps/dashrate/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# CLAUDE.md

This file provides guidance to Claude Code when working in [example-apps/dashrate/](.).

## Project Overview

React + TypeScript + Vite app for rating and reviewing Dash Platform resources (tutorials and example apps) on testnet. A review has a required integer `rating` (1–5), an optional `reviewText`, and a `resourceId` pointing at a catalog entry. One review per identity per resource (enforced by a unique index) — saving again edits the existing document. The shell is a four-view app (`resources` / `my-reviews` / `settings` / `how`): the Resources view is a sidebar resource list + a detail panel showing the aggregate rating, a per-star distribution histogram, a review form, and recent reviews. Read-only browse works without auth; writing a review requires signing in with a mnemonic in Settings.

This app is the showcase for Platform 4.0's relational query features — provable `count`, grouped `count` (`GROUP BY`), range counts, and `where` filtering. See [SDK query patterns](#sdk-query-patterns).

## Commands

- `npm run dev` — start Vite dev server
- `npm run build` — typecheck (`tsc -b`) then bundle
- `npm run lint` — ESLint
- `npm run test` — Vitest suite in [test/](test/) (unit, component, and hook tests)
- `npm run test:coverage` — Vitest suite under v8 coverage
- `npm run test:e2e` — Playwright suite in [test/e2e/](test/e2e/) (auto-boots Vite on :5182)
- `npm run test:e2e:ui` — Playwright with the interactive UI runner
- `npm run format` / `format:check` — Prettier
- `npm run preview` — serve production build locally

## Architecture

`App.tsx` is the orchestrator: it owns the cross-cutting state and handlers, calls the data hooks, and renders one presentational view component per `view`. The layers:

- **[src/dash/](src/dash/)** — one file per Platform SDK concern, each with a leading JSDoc block naming the SDK method it wraps: [contract.ts](src/dash/contract.ts) (schema + register/store contract ID), [queries.ts](src/dash/queries.ts) (all reads — count, grouped count, document query, normalization, summary derivation), [review.ts](src/dash/review.ts) (create/replace a review), [history.ts](src/dash/history.ts) (document revision history), [resolveDpnsName.ts](src/dash/resolveDpnsName.ts) (DPNS username lookup for reviewer names, via `sdk.dpns.username`), [types.ts](src/dash/types.ts) (shared SDK type aliases incl. the `DashSdk` shape), [sdkModule.ts](src/dash/sdkModule.ts) (cached dynamic `import("@dashevo/evo-sdk")`), [sdkCore.ts](src/dash/sdkCore.ts) (cached dynamic `import` of the shared core at `../../../../setupDashClient-core.mjs`, exposing `loadSdkCore()`), [client.ts](src/dash/client.ts) / [keyManager.ts](src/dash/keyManager.ts) (re-export `createClient` / `IdentityKeyManager` from that shared core).
- **[src/catalog/resources.ts](src/catalog/resources.ts)** — the hardcoded `RESOURCES` list (tutorials + example apps). Each has `id` (used as `resourceId`), `title`, `category`, `summary`, `href`. Resources are compile-time static — there's no user-facing "add a resource" flow.
- **[src/App.tsx](src/App.tsx)** — owns top-level state (session, `contractId`/`contractInput`, `selectedResourceId`, `view`, sign-in form fields, `history`, `status`, `busy`), the action handlers (`handleSignIn`, `handleSaveReview`, `handleRegisterContract`, `handleLoadHistory`, contract submit/clear, sign-out, resource select, edit-my-review), and the `connectReadOnly` read-only SDK factory. It loads the shared SDK core via [sdkCore.ts](src/dash/sdkCore.ts)'s `loadSdkCore()`.
- **[src/hooks/](src/hooks/)** — data-fetching hooks, each returning state plus refresh callbacks: [useResourceRatings.ts](src/hooks/useResourceRatings.ts) (per-resource summaries/distributions, recent reviews, the review-composer state — `rating`/`hoverRating`/`reviewText`/`mySelectedReview` — `reviewFilter`, and the `loadResourceData`/`refreshReviews` effects with request-id guards against stale responses), [useMyReviews.ts](src/hooks/useMyReviews.ts) (the signed-in user's reviews + derived average, loaded lazily when the `my-reviews` view is active), [useDpnsNames.ts](src/hooks/useDpnsNames.ts) (best-effort DPNS name cache keyed by owner ID, resolved lazily for reviewers in view).
- **[src/components/](src/components/)** — presentational components, props-only (no SDK imports): the four view shells [ResourcesView](src/components/ResourcesView.tsx), [MyReviewsView](src/components/MyReviewsView.tsx), [SettingsView](src/components/SettingsView.tsx), [HowItWorks](src/components/HowItWorks.tsx); [TopNav](src/components/TopNav.tsx) (owns the `View` type); [AppNotices](src/components/AppNotices.tsx) (status banner + "no contract" notice); and the pieces [ReviewForm](src/components/ReviewForm.tsx), [RecentReviews](src/components/RecentReviews.tsx), [ReviewRow](src/components/ReviewRow.tsx), [MyReviewCard](src/components/MyReviewCard.tsx), [ReviewHistory](src/components/ReviewHistory.tsx), [StarMeter](src/components/StarMeter.tsx) (partial-fill star renderer).
- **[src/session/types.ts](src/session/types.ts)** — the `Session` shape (`{ sdk, keyManager, identityId }`) shared by App and the hooks.
- **[src/lib/](src/lib/)** — pure utilities, no SDK references: [logger.ts](src/lib/logger.ts) (`Logger`/`LogLevel` types, `errorMessage`, `consoleLogger`), [format.ts](src/lib/format.ts) (`formatAverage`, `formatDate`, `shortId`), [ratings.ts](src/lib/ratings.ts) (`RATING_ROWS`, `emptySummary`/`emptyDistribution`, `ownerLabel`, `reviewCountLine`, text `stars`).
- **[test/](test/)** — Vitest + Testing Library, flat directory, named after the subject. Three kinds: unit tests over `src/dash/` and `src/lib/` that stub the `DashSdk` shape (`*.test.ts`); component tests rendered with Testing Library (`*.test.tsx`); and hook tests via `renderHook`. Default Vitest env is `node`; component/hook tests opt into DOM with a `// @vitest-environment jsdom` pragma at the top of the file (the vitest `include` covers `**/*.test.{ts,tsx}`). Component/hook tests **mock the SDK loaders** (`vi.mock("../src/dash/sdkModule", …)` / `vi.mock("../src/dash/sdkCore", …)`) so the 8 MB bundle never imports — never let a test pull `@dashevo/evo-sdk` into the jsdom process. [tsconfig.app.json](tsconfig.app.json) includes `test`, so `tsc -b` (run by `build`) strict-typechecks the `.test.tsx` files — keep mock factories and stub props fully typed. `npm run test:coverage` runs the suite under v8 coverage.
- **[test/e2e/](test/e2e/)** — Playwright specs plus shared `fixtures.ts`, driven by [playwright.config.ts](playwright.config.ts), which auto-starts `npx vite` on port 5182. Two projects (`chromium-desktop` / `chromium-mobile`) so every spec exercises both layouts. Runs against real testnet — no SDK mocks. The specs are read-only shell smoke tests ([smoke](test/e2e/smoke.spec.ts) — navigation, browse a resource, How-it-works; [settings](test/e2e/settings.spec.ts) — the sign-in form renders/gates without signing in); they assert rendering, not live rating data. Run locally via `npm run test:e2e`. [tsconfig.app.json](tsconfig.app.json)'s `exclude: ["test/e2e"]` keeps these out of the app `tsc -b` (Playwright typechecks them itself).

## Review contract

Schema lives in [src/dash/contract.ts](src/dash/contract.ts) as `REVIEW_SCHEMAS`. One document type, `review`:

- `resourceId` — required string, 1–63 chars, position 0
- `rating` — required integer, 1–5, position 1
- `reviewText` — optional string, max 1000 chars, position 2
- `$createdAt`, `$updatedAt` — required (Platform-managed)
- `documentsMutable: true`, `documentsKeepHistory: true`, `canBeDeleted: false` — reviews are editable, keep revision history, and can't be deleted

Indices:

- `ownerAndResource` — unique (`$ownerId`, `resourceId`); enforces one review per identity per resource
- `ownerReviews` — (`$ownerId`, `$updatedAt`); lists a user's reviews
- `resourceRatingAggregate` — (`resourceId`), `countable`; total review count per resource
- `resourceRatingDistribution` — (`resourceId`, `rating`), `countable` + `rangeCountable: true`; backs the grouped rating distribution AND the `rating == N` filter

`DEFAULT_CONTRACT_ID` is `BdgTqaTAPYMyhp1WdeWdcvYSgoD7AuJ7tVCaCSXyQgyP`. Overrides are stored under `localStorage['dashrate.contractId']`. Settings can register a fresh contract and switch to it; the contract-ID input is controlled and auto-fills on register.

## SDK query patterns

This app deliberately demonstrates the relational query surface. The query types in play:

- **Total count** — `sdk.documents.count({ where: [["resourceId","==",id]] })` over the single-property `resourceRatingAggregate` index. Ungrouped result is a one-entry `Map` keyed `""`; read with `firstMapValue`. (`getRatingCount` in [queries.ts](src/dash/queries.ts).)
- **Grouped distribution count** — `sdk.documents.count({ where: [["resourceId","==",id], ["rating","between",[1,5]]], groupBy: ["rating"], orderBy: [["rating","asc"]] })` over `resourceRatingDistribution`. Returns one entry per present rating. The average is **derived in JS** from these per-star counts (`summaryFromDistribution`) — there is no `sum`/`average` query. (`getRatingDistribution` in [queries.ts](src/dash/queries.ts).)
- **Filter by rating** — `listResourceReviews` adds `["rating","==",N]` to the `where` (a point lookup covered by `[resourceId, rating]`). Server-side on purpose, to demonstrate `where` filtering and stay correct past the fetch limit.
- **Document query / history** — `sdk.documents.query` for the review list; `sdk.documents.history` for revisions.

`normalizeReviews` / `normalizeSingleReview` in [queries.ts](src/dash/queries.ts) flatten whatever shape `query`/`get` returns (array, Map, plain object) into `ReviewRecord[]`.

## Performance — load-anchor rules

Same as the sibling apps: the `@dashevo/evo-sdk` browser bundle is ~8 MB and must stay off the boot critical path. **Never add a top-level value import from `@dashevo/evo-sdk`** to any file reachable from `App.tsx` — go through [sdkModule.ts](src/dash/sdkModule.ts)'s cached dynamic import (type-only imports are fine). The shared core is loaded via [sdkCore.ts](src/dash/sdkCore.ts)'s `loadSdkCore()` — two distinct loaders, don't merge. The `modulePreload.resolveDependencies` filter in [vite.config.ts](vite.config.ts) strips the `evo-sdk` chunk so Vite doesn't inject a `<link rel="modulepreload">` that re-blocks first paint. The synchronous exports of [contract.ts](src/dash/contract.ts) (`REVIEW_SCHEMAS`, `loadStoredContractId`, `saveContractId`, `clearStoredContractId`, `DEFAULT_CONTRACT_ID`) must stay synchronous — they run during initial render before the SDK loads.

## Gotchas

- **Grouped-count map keys are raw index-key bytes, NOT the value.** `count` with `groupBy: ["rating"]` returns a `Map` keyed by the hex of the property's order-preserving index-key encoding, not the integer. For a small positive integer that's the sign-flipped single byte `0x80 | value` → rating 5 is key `"85"`, rating 1 is `"81"` (verified against the live contract — it is _not_ an 8-byte big-endian form). `ratingKeyHex(r) = (0x80 | r).toString(16)` in [queries.ts](src/dash/queries.ts) re-encodes each known rating to look it up. The SDK exposes no decoder, so the client encodes the values it's looking for rather than decoding what comes back.
- **`rangeCountable` is a separate flag from `countable`, required for range/grouped counts.** A range count (the `between` on `rating` that drives the grouped distinct walk) needs `rangeCountable: true` on the index, with the range field as the **last** index property. A `countable`-only index fails at query time with `range count requires a range_countable: true index whose last property matches the range field`.
- **Don't mix `summable` and a deeper count-only index on a shared prefix** — it registers fine but breaks every document insert (`NotCountedOrSummed-wrapping is only supported for the six sum-bearing tree variants`). `resourceRatingAggregate` is intentionally count-only (no `summable`) so its `resourceId` value tree isn't count+sum and can host `resourceRatingDistribution`'s count-only `rating` continuation. The average is derived from the distribution instead of a `sum`/`average` query. Full analysis: [dashpay/platform#3960](https://github.com/dashpay/platform/issues/3960).
- **`between` value is a 2-element array, inclusive.** `["rating","between",[1,5]]` matches `1 <= rating <= 5`. The drive expects exactly two bounds.
- **A document query's `orderBy` field must be the serving index's TRAILING property — even for equality filters.** The index matcher (`Index::matches`) reserves the order-by field from the _back_ of the index. So filtering reviews by rating (`where resourceId== AND rating==`) must use `orderBy: [["rating","asc"]]` (the last property of `[resourceId, rating]`); ordering by `resourceId` there strips `rating` from the usable prefix and the query is rejected as `where clause on non indexed property … query must be for valid indexes`. `listResourceReviews` switches `orderBy` based on whether a `ratingFilter` is set. (Server `orderBy` only drives index selection here; the list is re-sorted client-side by `createdAt`.)
- **Update flow** (`saveReview` replacing an existing review) bumps the revision off the fetched document; the unique `ownerAndResource` index means a second save edits rather than duplicates.
- **The contract-ID input is controlled** (`contractInput` state in `App.tsx`). Register/Use/Clear all sync it; an uncontrolled `defaultValue` would not reflect a freshly-registered ID.
- The Evo SDK WASM bundle is ~8 MB; that's expected, not a build error. See [Performance](#performance--load-anchor-rules).
61 changes: 61 additions & 0 deletions example-apps/dashrate/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# DashRate

DashRate is a React + TypeScript + Vite example app for rating Platform tutorial resources on Dash
Platform testnet.

It is intentionally small and centered on Platform v4's relational document queries:

- `sdk.documents.query` for resource reviews, identity reviews, and existing-review lookup
- `sdk.documents.count` for the total review count per resource (a plain countable index)
- `sdk.documents.count` with `groupBy: ["rating"]` for the per-star rating distribution — the
count/sum/average shown per resource is derived in JS from this one grouped count, not a separate
`sum`/`average` query
- a `where rating == N` clause for filtering reviews by star rating
- `sdk.documents.history` for review edit history

Users sign in with a mnemonic only. After signing in, they can register a local testnet contract,
paste an existing DashRate contract ID, create one review per resource, edit that review, and
inspect its document history. Read-only browsing (resources, aggregates, reviews) works without
signing in.

## Quick start

```bash
npm install
npm run dev
```

Other scripts:

```bash
npm run build
npm run test
npm run test:coverage
npm run lint
npm run preview
```

## Contract

The app defines one mutable document type, `review`, in
[`src/dash/contract.ts`](./src/dash/contract.ts). Each identity can review a resource once, enforced
by the unique `$ownerId + resourceId` index. Saving a review creates the document on first use and
replaces the same document on later edits, with document history retained by `documentsKeepHistory`.

The read paths are intentionally index-shaped:

- resource detail and recent reviews query by `resourceId`
- My reviews queries by `$ownerId` and sorts by `$updatedAt`
- edit detection queries by `$ownerId + resourceId`
- the total review count uses the standalone `resourceId` index (`countable: "countable"`)
- the rating distribution and the `rating == N` filter use the compound `resourceId + rating` index
(`countable: "countable"` plus `rangeCountable: true`)

Neither aggregate index uses `summable`: the count/sum/average shown per resource is computed in JS
from the grouped distribution count, so a single grouped `count` query backs both the histogram and
the average.
Comment on lines +50 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify which aggregate values come from the grouped count.

Lines 50-52 say the total review count uses the standalone resourceId count index, but Lines 54-56 then say the displayed count/sum/average all come from the grouped distribution query. Those two descriptions can't both be true, so the README is currently teaching the wrong query plan.

✏️ Suggested wording
-Neither aggregate index uses `summable`: the count/sum/average shown per resource is computed in JS
-from the grouped distribution count, so a single grouped `count` query backs both the histogram and
-the average.
+Neither aggregate index uses `summable`: the displayed average is computed in JS from the grouped
+distribution count, while the total review count comes from the standalone `resourceId` count
+index.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- the total review count uses the standalone `resourceId` index (`countable: "countable"`)
- the rating distribution and the `rating == N` filter use the compound `resourceId + rating` index
(`countable: "countable"` plus `rangeCountable: true`)
Neither aggregate index uses `summable`: the count/sum/average shown per resource is computed in JS
from the grouped distribution count, so a single grouped `count` query backs both the histogram and
the average.
- the total review count uses the standalone `resourceId` index (`countable: "countable"`)
- the rating distribution and the `rating == N` filter use the compound `resourceId + rating` index
(`countable: "countable"` plus `rangeCountable: true`)
Neither aggregate index uses `summable`: the displayed average is computed in JS from the grouped
distribution count, while the total review count comes from the standalone `resourceId` count
index.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@example-apps/dashrate/README.md` around lines 50 - 56, The README description
of aggregate queries is inconsistent about where the total review count comes
from; update the wording in the dashrate README to make the query plan
consistent. Use the existing aggregate/index terms already mentioned there
(`resourceId`, `rating`, `countable`, `rangeCountable`, `summable`) and clarify
whether the standalone count index is only for total counts or whether the
grouped count query is the single source for histogram, sum, and average so the
reader sees one coherent explanation.


`DEFAULT_CONTRACT_ID` is set to a published testnet DashRate contract
(`BdgTqaTAPYMyhp1WdeWdcvYSgoD7AuJ7tVCaCSXyQgyP`), so fresh installs can read aggregates and reviews
immediately. The active ID is stored under `localStorage['dashrate.contractId']`; clearing it falls
back to this default. Register your own contract from the Settings tab to override it.
Loading