Skip to content

ship: dev → main (share-card UX + GSC Dataset fixes + share-modal dimension pickers) - #1298

Closed
Flotapponnier wants to merge 132 commits into
mainfrom
dev
Closed

ship: dev → main (share-card UX + GSC Dataset fixes + share-modal dimension pickers)#1298
Flotapponnier wants to merge 132 commits into
mainfrom
dev

Conversation

@Flotapponnier

Copy link
Copy Markdown
Collaborator

Promoting dev to prod. Highlights over the last cycle:

Share-card (image generator) — Maxime UX pass

GSC Dataset structured-data warnings — closed the 3 non-critical flags

Plus TimeSeriesChart tweaks (#1279, #1290) and small chore batches. Full log: git log main..dev.

Florent Tapponnier and others added 30 commits July 12, 2026 21:39
…#1136)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Co-authored-by: Florent Tapponnier <contact@mobula.io>
Robinhood Chain: hub entry + 3 bench integrations
…cution mutex primitive

DailySpentUSD only ever incremented and was zeroed by every restart, so a
crash-loop minted a fresh budget each time. The tracker resets exactly at
UTC midnight, persists (date, spent) to SPEND_STATE_PATH (default
./spend-state.json, not /tmp) and is mutex-guarded. Also declares the
package-level single-flight lock and the FAILED_TX_FEE_ESTIMATE_USD
(default 0.50) helper for gas bled by failed broadcasts.
…get, single-flight lock

Fund-loss fixes from hostile review:
- a corrective transfer whose deposit broadcast but whose bridge status
  never resolved is now TERMINAL for the scheduler slot (funds are likely
  still in flight, a retry double-sends); only pre-broadcast failures may
  consume another attempt. executeOnBridge keeps TxHash on error paths so
  callers can tell the two apart.
- ONE corrective-transfer budget (max 2) per scheduler slot, shared across
  all downgrade-ladder rungs (was per rung: up to 6 transfers per slot).
- package-level execution mutex: scheduler slots own the wallets end to
  end, the reaper TryLocks and skips its tick instead of queueing.
- broadcasts that failed or timed out after getting a TxHash book a flat
  FAILED_TX_FEE_ESTIMATE_USD against the daily cap instead of bleeding
  unaccounted gas; all spend goes through the persisted UTC-day tracker.
- gas top-up now honors MaxDailySpendUSD before broadcasting.
The reaper (and any other consumer) used to accept simulated balances even
in production with broadcast-capable keys, so phantom numbers could
green-light real transfers. SimulateBalances now takes the execution mode
and returns nil outside dry-run; startup logs a warning when the flag is
set in a broadcast-capable mode.
bridge-monitor: self-healing execution loop (dormant)
…st-fill (#1141)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Co-authored-by: Florent Tapponnier <contact@mobula.io>
tokenized-stock-peg: bench 076, first onchain equity pricing bench
…he miss (bnb-rpc awaiting incident) (#1144)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
tokenized-stock-peg: fix spark parser for the flat response shape
tokenized-stock-peg: spec validation fix + methodology hardening
Flotapponnier and others added 28 commits July 17, 2026 14:52
…ation (#1257)

GSC flagged 'Missing field name' on aggregator-head-lag JSON-LD (and by extension every bench slug page + every per-chain page + the methodology hub — all TechArticle nodes shipped only 'headline'). Schema.org Article inherits 'name' from CreativeWork; Google's Rich Results validator treats it as required even when 'headline' is present. Add 'name' alongside 'headline' with the same value on all three affected builders.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…le (#1260)

Feedback from Maxime: exported images do not adapt to the active
filters on the bench page. A share-card exported from
/benchmarks/rpc-capabilities?chain=ethereum&region=sgp used to render
just 'Fastest free public RPC ...' with cross-chain data; now the
loader picks up the region filter and the title reads 'Fastest free
public RPC ... Ethereum · Singapore' with the correctly scoped data.

Change:
  * share-card route reads region, kind, venue URL params (symmetric
    with the existing chain param). Each maps to a spec dimension and
    forwards to the materialize loader so the data is filtered too,
    not just the title.
  * A composed contextParts string is built from the resolved dimension
    labels and appended to the aggregate title. Empty when no filter
    is active, no trailing separator on the default view.
  * The composed title is overlaid onto the benchmark passed to each
    render (spread with {...b, title: displayTitle}) so all 5 templates
    (ranking, leaderboard, snapshot, headline, compare) pick it up
    without any render-signature changes.
  * share-section + share-section-modal read live URL params for
    region/kind/venue in addition to chain when composing the card URL,
    so a modal opened from a scoped bench page passes the same scope
    to the exported PNG.

Follow-up (not in this PR): view / range / topN selectors, per-bench
UI in the modal to expose the dimension pickers explicitly. This PR
is the plumbing; the modal UX for exposing new pickers is next.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…amic title (#1260)" (#1261)

This reverts commit b0ce3ef.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…pose dynamic title (#1260)" (#1261)" (#1263)

This reverts commit d7f0fcb.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…l-silent false positives (#1265)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…enches) (#1266)

Commit c03c599 relaxed the sortByP50 filter from `p50 > 0` to `p50 !== 0` to support the negative-p50 'warming up' sentinel introduced for funding benches on the /products page. The relaxation leaked into the share-card render path, where negatives crash Satori: negative p50 values fed to Math.max produce a negative maxP50, which inverts every bar height ratio and blows up the JSX with an opaque 'Spread syntax' error mid-render.

Symptoms observed: /benchmarks/{rpc-capabilities,polkadot-rpc,ws-head-latency-ethereum,evm-block-builders}/share-card?template=ranking all returning HTTP 500. Working benches (network-coverage, perp-fees, oracle-deviation) had no negative sentinel values in their data.

Fix restores `p50 > 0` in the share-card filter. The negative sentinel handling stays on the /products page where it was designed to render (as 'warming up'). Share-card treats negatives as noise and drops them, same as zeros.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…e) (#1267)

Feedback from Maxime: user needs to be able to pick chain/region/kind/venue from within the share modal, not just inherit whatever the URL had when opening. Also the composed card title must reflect the pick in real-time.

Adds a DimensionRow chip component rendered above the template tabs. One row per dimension the bench declares (chainDims / regionDims / kindDims / venueDims). Each row: 'Label' + 'All' chip + one chip per option. Clicking a chip toggles state; clicking the active pick clears back to All.

State is seeded from the live URL on mount so the modal opened from a scoped bench page starts on the same scope (unchanged behaviour). User can override in-modal by clicking a different chip; state updates re-render the card preview with the new filter forwarded to the loader.

The cardSrc URL builder now reads from picker state instead of the live URL - previous implementation re-read the URL every render which meant the modal's own state changes couldn't propagate. Same URL param names on the share-card route: no server-side change needed.

Bench without dimensions: block hidden entirely, modal looks unchanged.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
… false positives (#1269)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…lay (#1270)

The region/kind/venue param plumbing (added via #1260 → reverted via
#1261 → re-added via #1263) and the display-title overlay via
{...b, title: displayTitle} still cause HTTP 500 on rpc-capabilities
share-card and other benches with dimensions. My sortByP50 fix
(#1266) turned out not to be the actual root cause; the runtime error
persists after that fix + the modal picker PR (#1267).

Reverts the route.tsx changes only:
  - Restore original getBenchmark({chain}) call (no region/kind/venue
    forwarding)
  - Restore original switch that passes plain benchmark/filteredSafe
    to renders (no {...b, title: displayTitle} spread)
  - chainLabel still passed through (unchanged from pre-Maxime-feedback)

Keeps the modal DimensionRow pickers (#1267) intact for a future
retry once the true root cause is found. They currently look for
matching URL params but the server-side won't act on them until the
route.tsx re-adds forwarding safely.

Bug still open on rpc-capabilities/polkadot-rpc/ws-head-latency-*/
evm-block-builders share-cards, but now user-visible functionality
(chain filter, existing templates on non-broken benches) is fully
restored to pre-Maxime-feedback behaviour.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…ST-only, no ws metric) (#1272)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…rashes) (#1273)

Two independent bugs causing pre-existing HTTP 500 on rpc-capabilities,
polkadot-rpc, ws-head-latency-*, evm-block-builders share-card
endpoints. Reproduced locally with bun dev + shimmed try/catch to
unmask the mid-stream Satori errors.

Bug 1: Satori (the JSX-to-SVG renderer next/og uses) only decodes
PNG / JPEG / SVG. Feed it AVIF or WebP bytes and it throws
'TypeError: u2 is not iterable' mid-render, surfacing as opaque
HTTP 500. publicnode.avif, drpc.webp, lava.webp all trigger this on
the RPC benches. Fix: skip AVIF/WebP extensions in
getProviderLogoDataUrl, fall back to the initials chip.

Bug 2: chipBackground / chipTextColor from src/lib/brand.ts return
CSS var references (e.g. 'var(--color-ink-soft)') when the provider
has no registered brand color. Satori evaluates var(...) to the
CSS-wide 'initial' keyword and rejects 'background: initial',
crashing the whole PNG. evm-block-builders is a full-cohort case
(Titan, BuilderNet, Quasar, Eureka, Builder+, Vanilla — none have
brand colors or logos). Fix: resolve var(...) fallbacks to the
concrete INK_SOFT / PAPER hex from the route's own palette.

Both fixes scoped to share-card route.tsx. src/lib/brand.ts untouched
(still used by the HTML surfaces where CSS variables resolve at
browser render time).

Verified locally: 40/40 renders (8 slugs x 5 templates) return
HTTP 200 with valid 1200x630 PNGs.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…h re-fetches (#1274)

* TimeSeriesChart: forward chain prop to /api/series so chain switch re-fetches

* chore: unstage branch-purge archive committed by mistake from another agent's untracked file

---------

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…itles) (#1275)

Screenshot from user: rpc-capabilities leaderboard template renders the
101-char title 'Fastest free public RPC for Ethereum, BNB, Polygon and
23 more chains (plus Solana and Polkadot)' at fontSize=50 which wraps
to 4 lines and overlaps the leaderboard rows underneath. Same crash on
snapshot (fontSize=48 title overflows into the time-series chart).

Fix: scaledTitleSize(base, title) helper ramps font size down for long
titles. <=60 chars stays at base, >100 chars shrinks to 50% base. Keeps
short titles at the original design size and only touches long ones.

Applied to 4 templates:
  ranking      44/56 base
  leaderboard  50 base
  snapshot     48 base
  compare      36 base
Headline stays untouched (title font is already 18 px because the
template features the winner's big number, not the bench title).

No layout math change, no CSS variable, no schema change. Just the
one font-size call per template swapped for the helper.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…them under sibling flex:1 (#1276)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
)

* share-card: flexShrink:0 on title+subtitle to stop Satori collapsing them under sibling flex:1

* share-card: cap leaderboard top 10 + flex-start (fix overflow overlapping title on wide cohorts)

---------

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Co-authored-by: Florent Tapponnier <contact@mobula.io>
…ly (#1279)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…les) (#1280)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…orts) (#1281)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…s legend was still bleeding into footer) (#1283)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…oter) (#1284)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
… → header pills (#1285)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…1286)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
… for themselves (#1290)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
… on products, alternatives, compare) (#1291)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
…laude local state (#1293)

Audit findings verified via grep (0 imports each):
- src/components/back-link.tsx
- src/components/hl-frontend-grid.tsx
- src/components/hl-history-chart.tsx
- src/components/logo-card-link.tsx
- src/components/report-section-modal.tsx
- src/components/share-section-modal.tsx

Untrack Claude harness per-session/per-user files that change on every
session and shouldn't be in VCS:
- .claude/scheduled_tasks.lock (agent runtime lock)
- .claude/settings.local.json (per-machine perms)

Adds matching .gitignore rules so they never come back. .claude/skills/*
stays tracked — those are intentional shared skill definitions.

Co-authored-by: Florent Tapponnier <contact@mobula.io>
… missing) (#1294)

Co-authored-by: Florent Tapponnier <contact@mobula.io>
Deep-audit confirmed via grep across the full repo (src/, harnesses/,
worker/, scripts/, packages/):

Fully deleted (0 refs anywhere):
- src/lib/time-constants.ts: MS_PER_SECOND, MS_PER_MINUTE, MS_PER_HOUR,
  MS_PER_DAY — never imported. SECONDS_PER_* stay (used by scales.ts).

Un-exported (only used internally in their own file):
- src/lib/search-featured.ts: FEATURED_SLUGS re-exported ALL_SLUGS but
  the alias itself had no external consumer.
- src/lib/categories.ts: CATEGORY_SLUG_BY_LABEL feeds only
  categorySlugFromLabel() one line below.
- src/lib/dataset-jsonld.ts: ZENODO_CONCEPT_DOI and ZENODO_CONCEPT_URL
  are only used to compose GLOBAL_DATASET_JSONLD and
  buildBenchDatasetJsonLd() in the same file.
- src/lib/materialize/schema.ts: SeriesRingSchema is only referenced
  by WorkerStateSchema's type inference (the SeriesRing type export
  stays — worker/index.ts imports it).

Co-authored-by: Florent Tapponnier <contact@mobula.io>
@Flotapponnier

Copy link
Copy Markdown
Collaborator Author

Closing - dev has 132 commits with 9 conflicts against main. Cherry-picking just the GSC Dataset fixes (#1291 + #1294) onto main instead.

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