Skip to Content
Internal docs are powered by Nextra Docs Theme.
SystemsPlatformAPI2Rails to API2 migrationPackagesDashboard, catalog and search reads

Dashboard, catalog and search reads

ENG-3131  moves eight Rulebase UI Rails-client read roots to native API2 routes. Query keys, infinite-query page parameters, loading and error states, filters, ordering, time-zone handling and aggregate denominators stay stable while API2 reads the Rails-owned public tables directly. There is no API2-to-Rails HTTP bridge, no Rails fallback and no runtime flag; rolling back the UI bundle restores the prior Rails reads.

Ownership boundary

UI rootRails read retiredAPI2 route
components/dashboard/style-guide/add-style-guide-link-dialog.tsxGET /notion_pagesGET /notion_pages
features/brands/api/get-brands.tsGET /zendesk_brandsGET /zendesk_brands (existing)
features/customer-issues/api/get-customer-issue-statistics.tsGET /customer_issues/{id}/statisticsGET /customer_issues/{id}/statistics
features/dashboard/.../scorecard-performance/justification-options-breakdown.tsxGET /scorecard_criteria/{id}/statisticsGET /scorecard_criteria/{id}/statistics
features/dashboard/.../scorecard-performance/scorecard-performance-criteria.tsxGET /scorecard_criteria/{id}/statisticssame
features/dashboards/api/get-check-results.tsGET /check_resultsGET /check_results
features/data-sources/api/get-data-sources.tsGET /data_sourcesGET /data_sources (existing)
features/search/api/search.ts (getSearchQueryOptions)GET /searchGET /legacy_search

The workspace search (/search on API2, getWorkspaceSearchQueryOptions) was already native and is unchanged. Rails keeps every write on these resources.

Contract notes that reconcile to Rails behaviour:

  • Date-only start_date/end_date parse in the organization time zone (start of day / end of day), must be ordered, and may span at most five years (src/lib/rails-date-range.ts). Daily buckets remain DATE(started_at) in UTC, as in Rails.
  • Customer-issue FCR rows are omitted only when root_count is below the mean root count across the window; fcr_rate = fcr_count / root_count and the adjusted rate is (1 - fcr_rate) * (root_count * 2).
  • Scorecard breakdowns use Rails’ three independent predicates (score = max_score, 0 < score < max_score, score = 0), so a max_score = 0 criterion counts as both passed and failed; the average excludes max_score = 0; the average series includes discarded children.
  • Check results include completed and active evaluations (overridden but still active evaluations stay), order by created_at DESC, id DESC, and agents only see evaluations whose subject employee is their own; an agent without a resolved employee fails closed.
  • Legacy search returns an empty Pagy envelope for a blank query, applies the Rails search policy scope, uses pg_search_documents with to_tsvector('simple', f_unaccent(...)), prefix and trigram matching, the numeric-identifier fast path, and the { id, name, type, caption } payload.
  • Notion pages decrypt the tenant’s first Notion data source secret, call POST /v1/search filtered to pages, drop blank titles, surface provider failures and cache per tenant for one hour.

Verification

Before merge: focused API2 PGlite acceptance (src/routes/dashboard-catalog-reads.db.test.ts, organizations 25 America/New_York and 41 Europe/Berlin), the ENG-3131 source gate (rulebase-ui/app/__tests__/lib/eng-3131-rails-source-gate.test.ts), the request-shape UI tests for brands, check results and search, API2 typecheck, Rulebase UI typecheck and lint. The OpenAPI export and generated rulebase-api2-schema.d.ts are committed with the routes.

Read-only reconciliation

Run against a read replica or in a read-only transaction (BEGIN READ ONLY;). Compare Rails and API2 responses for the same session, tenant and parameters during the bake window (through 2026-09-15 20:30 America/New_York) and for one full weekly window after the UI deploy. Nothing below mutates data.

Aggregate denominators — customer-issue statistics for one tenant and window (:org, :issue, :from, :to are tenant-local day bounds converted to UTC):

SELECT DATE(c.started_at) AS day, COUNT(*) AS conversations FROM conversations c WHERE c.organization_id = :org AND c.customer_issue_id = :issue AND c.started_at >= :from AND c.started_at <= :to GROUP BY 1 ORDER BY 1;

Scorecard criterion breakdown denominators:

SELECT r.qa_agent_evaluation_scorecard_criterion_id AS criterion_id, COUNT(*) FILTER (WHERE r.score = r.max_score) AS passed, COUNT(*) FILTER (WHERE r.score > 0 AND r.score < r.max_score) AS partial, COUNT(*) FILTER (WHERE r.score = 0) AS failed, AVG(r.score::numeric / NULLIF(r.max_score, 0)) AS average FROM qa_agent_evaluation_scorecard_criterion_results r JOIN qa_agent_evaluations e ON e.id = r.qa_agent_evaluation_id WHERE e.organization_id = :org AND e.processing_status = 'completed' AND e.active AND e.created_at >= :from AND e.created_at <= :to GROUP BY 1;

Check-result page totals (meta.page.total must match on both services):

SELECT COUNT(*) FROM qa_agent_evaluation_scorecard_criterion_results r JOIN qa_agent_evaluations e ON e.id = r.qa_agent_evaluation_id WHERE e.organization_id = :org AND e.processing_status = 'completed' AND e.active;

Catalog list totals:

SELECT COUNT(*) FROM zendesk_brands WHERE organization_id = :org; SELECT COUNT(*) FROM organization_data_sources WHERE organization_id = :org;

Search candidate counts for a query (:q), per searchable type:

SELECT searchable_type, COUNT(*) FROM pg_search_documents WHERE organization_id = :org AND to_tsvector('simple', f_unaccent(content)) @@ to_tsquery('simple', f_unaccent(:q) || ':*') GROUP BY 1;

Payload reconciliation: for each root, fetch the Rails and API2 responses with the same filter, page[page], page[limit], start_date/end_date and type values and diff them as JSON after sorting keys. Expected differences: none. Any missing/extra row, different meta.page.total, different ordering, or a numeric difference beyond floating-point rounding is a finding.

Rollout and rollback

Deploy API2 before the Rulebase UI. After both regional releases are healthy, watch the API2 route error rates and latency for /notion_pages, /customer_issues/:id/statistics, /scorecard_criteria/:id/statistics, /check_results and /legacy_search, and confirm the Rails NotionPagesController, CustomerIssues::StatisticsController, ScorecardCriteria::StatisticsController, CheckResultsController and SearchController#index transactions stop receiving first-party rulebase-ui requests through a complete production window.

Roll back the Rulebase UI bundle (Rails reads resume immediately) when any of these trigger:

  • any reconciliation query or payload diff above disagrees for a production tenant;
  • 5xx rate on any listed API2 route exceeds the Rails baseline for the same route over 15 minutes, or p95 latency doubles the Rails baseline;
  • a 403 for a session that Rails authorizes (agent, partner or group-scoped reads), or any cross-tenant row in a response;
  • Notion provider failures surface for a tenant whose Rails /notion_pages succeeds with the same data source.

API2 routes can stay deployed during a UI rollback; they are read-only.

Last updated on