midday-ai/midday
Reviewed against Rams quality heuristics: accessibility, color, typography, spacing, components, motion, UX, and craft.
30 files reviewed·July 21, 2026
More findings
Verdict
Composition is clean but resilience is applied selectively, one table gets an error boundary while its neighboring widgets get none. The real risk is silent failure masquerading as empty state, which quietly erodes trust in onboarding and inbox flows.
Files Rams reviewed
apps/dashboard/src/app/[locale]/(app)/(sidebar)/apps/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/customers/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/inbox/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/invoices/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/layout.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/settings/billing/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/settings/developer/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/settings/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/tracker/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/transactions/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/upgrade/page.tsx
apps/dashboard/src/app/[locale]/(app)/(sidebar)/vault/page.tsx
apps/dashboard/src/app/[locale]/(app)/oauth/authorize/page.tsx
apps/dashboard/src/app/[locale]/(app)/onboarding/page.tsx
apps/dashboard/src/app/[locale]/(app)/teams/page.tsx
apps/dashboard/src/app/[locale]/(public)/connectors/callback/page.tsx
apps/dashboard/src/app/[locale]/(public)/i/[token]/page.tsx
apps/dashboard/src/app/[locale]/(public)/login/page.tsx
apps/dashboard/src/app/[locale]/(public)/oauth-callback/page.tsx
apps/dashboard/src/app/[locale]/(public)/p/[portalId]/page.tsx
apps/dashboard/src/app/[locale]/(public)/r/[linkId]/page.tsx
apps/dashboard/src/app/[locale]/(public)/s/[shortId]/page.tsx
apps/dashboard/src/app/[locale]/layout.tsx
apps/website/src/app/agents/page.tsx
apps/website/src/app/chat/imessage/page.tsx
apps/website/src/app/chat/slack/page.tsx
apps/website/src/app/chat/telegram/page.tsx
apps/website/src/app/chat/whatsapp/page.tsx
apps/website/src/app/compare/[slug]/page.tsx
apps/website/src/app/compare/page.tsx
UX
apps/dashboard/src/app/[locale]/(app)/(sidebar)/inbox/page.tsx:31
A failed inbox or accounts fetch silently renders as 'not connected yet'
The comment at line 30 states the `.catch(() => null)` on both `fetchInfiniteQuery` and `fetchQuery` (lines 32-49) exists so a transient failure doesn't blank the page: a reasonable goal, but the tradeoff isn't handled: a rejected fetch and a genuinely empty result both resolve to `null`, so `hasConnectedAccounts` and `hasInboxItems` end up `false` either way. The very next branch (`!hasConnectedAccounts && !hasInboxItems && !hasFilter`) then renders `<InboxGetStarted />`, and an existing customer whose fetch simply failed sees the onboarding screen asking them to connect an account they already connected.
Why it matters
A transient API failure is indistinguishable from a brand-new user, so returning customers get onboarding copy and a 'connect your inbox' CTA instead of an error state with a retry action. The app already has an `ErrorFallback` component wired up for `InboxView` below, but these earlier return branches bypass it entirely.
Fix
Track fetch failure separately from an empty result and render the existing ErrorFallback with retry when either query rejects, instead of falling through to the onboarding or empty-state branches.
const [data, accounts] = await Promise.all([
queryClient
.fetchInfiniteQuery(
trpc.inbox.get.infiniteQueryOptions(
{
order: params.inboxOrder,
sort: params.inboxSort,
...filter,
tab: filter.tab ?? "all",
},
{
getNextPageParam: ({ meta }) => meta?.cursor,
},
),
)
.catch(() => null),
queryClient
.fetchQuery(trpc.inboxAccounts.get.queryOptions())
.catch(() => null),
]);const [dataResult, accountsResult] = await Promise.allSettled([
queryClient.fetchInfiniteQuery(
trpc.inbox.get.infiniteQueryOptions(
{
order: params.inboxOrder,
sort: params.inboxSort,
...filter,
tab: filter.tab ?? "all",
},
{
getNextPageParam: ({ meta }) => meta?.cursor,
},
),
),
queryClient.fetchQuery(trpc.inboxAccounts.get.queryOptions()),
]);
const data = dataResult.status === "fulfilled" ? dataResult.value : null;
const accounts =
accountsResult.status === "fulfilled" ? accountsResult.value : null;
if (dataResult.status === "rejected" || accountsResult.status === "rejected") {
return <ErrorFallback />;
}apps/dashboard/src/app/[locale]/(app)/(sidebar)/customers/page.tsx:66
Analytics widgets have no error boundary, unlike the customer table
The four summary widgets `MostActiveClient`, `InactiveClients`, `TopRevenueClient`, and `NewCustomersThisMonth` are each wrapped only in `Suspense` with a `CustomerSummarySkeleton` fallback. Below them, `DataTable` gets both `Suspense` and `ErrorBoundary errorComponent={ErrorFallback}`. If any of the four prefetched queries (`mostActiveClient`, `inactiveClientsCount`, `topRevenueClient`, `newCustomersCount`) throws, there's no boundary to catch it before the table's own boundary.
Why it matters
A failed analytics query crashes the whole route (or bubbles to the nearest ancestor boundary outside this page) instead of degrading gracefully, so the entire customers page can go blank over a single non-critical stat failing to load.
Fix
Wrap each summary widget (or the whole CollapsibleSummary grid) in the same ErrorBoundary/ErrorFallback pattern already used for the table.
<CollapsibleSummary>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 pt-6">
<Suspense fallback={<CustomerSummarySkeleton />}>
<MostActiveClient />
</Suspense><CollapsibleSummary>
<ErrorBoundary errorComponent={ErrorFallback}>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 sm:gap-6 pt-6">
<Suspense fallback={<CustomerSummarySkeleton />}>
<MostActiveClient />
</Suspense>apps/dashboard/src/app/[locale]/(app)/(sidebar)/inbox/page.tsx:68
The 'connected empty' branch has no way to tell sync failure from zero items
The second early return (lines 68-81) shows `<InboxConnectedEmpty />` whenever `hasConnectedAccounts && hasSyncedAccounts && !hasInboxItems`. Because `accounts` and `data` can both be `null` from the swallowed `.catch()` above, an accounts-fetch failure and 'you're synced but nothing came in yet' produce the exact same branch outcome: the user sees an empty inbox with no indication anything went wrong and no retry action.
Why it matters
Users can't distinguish 'nothing to review' from 'we couldn't check': a real outage reads as a quiet, empty inbox with no next step, so the user has no reason to reload or contact support.
Fix
Gate this branch on a fetch-succeeded flag as well as the emptiness checks, so a failed fetch never reaches the same UI as a confirmed-empty inbox.
if (
isAllTab &&
hasConnectedAccounts &&
hasSyncedAccounts &&
!hasInboxItems &&
!hasFilter &&
!params.connected
) {if (
isAllTab &&
hasConnectedAccounts &&
hasSyncedAccounts &&
!hasInboxItems &&
!hasFilter &&
!params.connected &&
dataResult.status === "fulfilled"
) {Components
apps/dashboard/src/app/[locale]/(app)/(sidebar)/customers/page.tsx:2
ErrorBoundary imported from a Next.js internal dist path, not a public API
Line 2 imports `ErrorBoundary` from `"next/dist/client/components/error-boundary"` instead of a supported package such as `react-error-boundary` or a project wrapper component.
Why it matters
Internal `dist` paths are not part of Next.js's public API and can move or change signature on any minor version bump, so a routine framework upgrade can silently break error handling on this page with no type error to catch it.
Fix
Import ErrorBoundary from a stable, versioned package or wrap it once in a project-owned component instead of reaching into framework internals.
import { ErrorBoundary } from "next/dist/client/components/error-boundary";import { ErrorBoundary } from "react-error-boundary";Get this score on every PR.
Rams reviews each pull request on your repo and posts inline one-click fixes — about a minute per review.
Install Rams freeCraft
apps/dashboard/src/app/[locale]/(app)/(sidebar)/apps/page.tsx:20
Unused prefetch client variable ships dead code to production
`const _queryClient = getQueryClient();` is assigned but never referenced anywhere in the component. The underscore prefix signals it was intentionally silenced rather than removed.
Why it matters
Dead variables like this accumulate silently in server components and make it harder for the next reader to tell whether the query client is actually needed for hydration or was left over from a refactor, increasing maintenance cost with every future edit to this file.
Fix
Remove variables that are assigned but never read, or use the value if it's actually required for hydration setup.
const _queryClient = getQueryClient();getQueryClient();Accessibility
Typography
Color
Spacing
Motion
Working well
- Wrapping `<Apps />` in both `Suspense` (with `AppsSkeleton` as fallback) and `ErrorBoundary` (with `ErrorFallback`) gives this route designed loading and error states instead of a blank screen or an uncaught crash. This is exactly the pattern UX literature asks for: every reachable state (loading, error, loaded) gets a deliberate rendering, not just the happy path.
- The Suspense fallbacks are matched to their actual content shape: InvoiceSummarySkeleton for the three summary cards and the distinct InvoicePaymentScoreSkeleton for the score widget, rather than one generic spinner for all four. That keeps the loading layout visually stable and predicts the real content's shape instead of jarring on swap-in.
- Each summary widget (MostActiveClient, InactiveClients, TopRevenueClient, NewCustomersThisMonth) gets its own Suspense boundary rather than one shared boundary around the whole grid. This means a slow query for one stat doesn't hold up the other three from rendering, which is exactly how independent data sources should stream.
- Batching all six `trpc` prefetch calls into a single `batchPrefetch` array before render, rather than firing them sequentially inside child components, keeps the data-fetching waterfall flat. This is a maintenance win as much as a performance one: new prefetches get added to one list instead of scattered across the tree.
Scored July 21, 2026 with Rams Engine v0.0.2 · Engine changelog
Score your own repo.
Free on public repos, no account. The same engine that scored this page reads your UI code and mints a score page like this one.
Public repos only. The full engine reviews the UI code and mints a public score page — we email you the link too. Already-scored repos open instantly.
Or get this review on every PR.