The Device type declared a `status` field that the schema does not have
and the API never sends, so `device.status === 'online'` was always
false. The badge took its colour from that phantom field and its text
from `enabled`, so an enabled, working device was styled exactly like a
disabled one. Both now come from `enabled`.
Devices, API keys and webhooks each rendered a bare unstyled
"Error: {error.message}" with no way to recover, putting raw transport
strings like "Request failed with status code 500" in front of users.
formatError already handles axios rejections and rate limits and was
used by none of them.
Adds components/shared/error-state.tsx as the counterpart to the
existing EmptyState, routing all three through formatError and offering
a retry.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The header quick action opened the API key modal directly, so a first-time
user who clicked "Add device" was handed a generated key with no mention of
installing the app or granting SMS permissions. The Registered Devices card
already had the right flow: explain the steps, then Continue into the key and
QR code.
That flow lived as local state inside device-list, so it could not be reused.
Extracted to add-device-dialog with the same imperative open() handle the
existing GenerateApiKey uses, and wired into both entry points.
"New API key" deliberately still goes straight to key generation. That button
asks for a key, so the device prerequisites would be noise there. The two
actions now do genuinely different things instead of both opening the same
modal under different labels.
E2e covers both entry points reaching the instructions first and Continue
still reaching key generation, plus New API key skipping the instructions.
Verified the guard fails against the old wiring rather than assuming it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dashboard:
- Removed the recent activity section.
- Corrected the usage card wording. The number was labelled "sent", but
getCurrentSubscription counts SMS documents with no type filter, and the
same model stores both SMSType.SENT and SMSType.RECEIVED. Inbound messages
consume the quota too, so the cards now say "messages" and carry an explicit
note that sends and receipts both count against the limit.
- "This month" is now "Last 30 days". The backend counts from setMonth(-1), a
rolling window, not the calendar month.
Devices:
- Removed the battery and signal indicators. Neither the device schema nor the
Android app reports either value, so they only ever rendered "unknown" and
"-" beside an icon that implied real telemetry. Also dropped the phantom
batteryLevel field from the Device type so nothing is built on it later.
API guide, rebuilt:
- It rendered a Collapsible that defaulted to closed. Once it moved to its own
route the page looked empty until you found the chevron. It is a page now,
so nothing is collapsed.
- Samples are generated from the user's real device id, with a device picker,
so they run after pasting one key. Every sample previously said
YOUR_DEVICE_ID, so nothing on the page was runnable as-is.
- Code blocks follow the theme. The old ones hardcoded a slate-800 background,
so they were the only dark element on a light page.
- Languages went from 3 to 5 (cURL, Node, Python, PHP, Go), picked once and
applied to every endpoint, replacing a hand-rolled inline Python SVG icon.
- Endpoints now cover bulk send and received messages, not just send and two
lookups. Response examples use the real lowercase status values from the SMS
schema instead of invented uppercase ones, and the batch example is no
longer invalid JSON (it had a trailing comma).
- Keys the copy state per block rather than by index arithmetic over
Object.entries order.
The mount guard usually needed for next-themes is not used in CodeBlock:
resolvedTheme is undefined on the server and first client render, so both
produce the light style. Verified with a console sweep across nine pages in
both colour schemes, zero messages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The home page was four all-time counters that could not answer either
question a user actually opens the dashboard for: how much quota is left, and
did my recent sends work.
Removed fabricated data:
- Every stat card rendered a green TrendingUp arrow whenever a value existed.
Nothing computed a trend. The stats endpoint returns running totals with no
time window, so there was nothing to compare against.
- "Since last year" on the sent and received counts. getStatsForUser sums
device counters with no date filter, so these are all-time totals.
- "Active Devices" with a "Connected now" caption, over a number that counts
every device including disabled ones. Enabled count is now derived from the
device list we already fetch.
- "Active keys" over a count that included revoked keys; now uses the active
API key list.
Added, all from fields the API already returns:
- Usage cards leading the page: today and this month against their limits,
with progress bars, an amber near-limit state past 80% and an upgrade link.
Unlimited plans (-1) show usage with no meter, since there is no ceiling to
measure against.
- Recent activity: the last 5 messages for the connected device. Messages are
only exposed per device, so the card names the device rather than implying
account-wide coverage.
- All-time totals compacted from four large cards into one honest strip.
lib/usage.ts extracts the daily/monthly derivation that subscription-info
already did inline, so the dashboard and billing page cannot disagree about
remaining quota. Unit tested including the -1 sentinel, custom limit
overrides, and an over-100 percentage (possible when a limit is lowered
mid-period).
No backend changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Get Started card is the activation funnel; this makes it exceptional:
- animated progress bar + "x of 6" + momentum copy that changes with
progress; per-step time-estimate chips; benefit-led step copy
- focus model: completed steps collapse to compact checked rows (check-pop
animation), only the active step expands with one primary CTA; staggered
entrance; auto-advance when the poll detects a step completed
- register_device is now self-serve inline: numbered instructions beside an
in-place generate-key + QR panel (reuses the existing key mutation and
react-qr-code); help dialog demoted to a "Need help?" fallback
- verify_email shows the actual email and an inline resend with a 60s
cooldown via the existing sendEmailVerificationEmail endpoint
- completion is celebrated (one-time success state with "Send a message"
CTA) instead of the card vanishing mid-glance
- minimize control collapses the card to a slim resumable progress pill
(localStorage), so setup never nags
Fail-closed states (bug fixes):
- previously a backend failure rendered the checklist from missing data,
showing verified users stuck on "Verify your email"; now a status machine
(loading/error/ready/hidden/celebrate) only renders the checklist when all
three queries succeeded; error shows a quiet retry row
- the full-height 6-row loading skeleton is replaced by a compact ~90px
placeholder; background poll failures keep last-good data
Tests: MSW handlers for onboarding PATCH + resend; RTL tests for the error
policy, mid-funnel progress, and all-done progress; e2e asserts the progress
bar on Home. Build, 28 unit tests, and 13 e2e green; verified at 375px.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- personal greeting hero (first name) with a quick-actions row: Send SMS
(primary, -> messaging), Add device and New API key (both open the
generate-key/QR flow; "Add device" keeps the user's mental-model label),
and Quick Start
- webhooks summary row: active-webhook count linking to /dashboard/webhooks,
keeping a discoverable mobile path now that Webhooks left the tab bar and
its section moved off Home
- Devices and API Keys cards show counts in their headers
- mobile-first: actions wrap under the greeting at 375px; verified visually
Build, 25 unit tests, and 13 e2e green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- extract the provider tree into app/(app)/providers.tsx and create the
QueryClient once via useState (it was rebuilt on every render, discarding
the cache); remove the old layout-wrapper
- replace the per-navigation whoAmI session check with a global 401 response
interceptor in httpBrowserClient that routes expired sessions to /logout
- rename middleware.ts -> proxy.ts (Next 16 convention), clearing the
deprecation warning; auth guard verified by e2e
- fix .env.example: NEXTAUTH_SECRET (was mislabeled AUTH_SECRET) + NEXTAUTH_URL
- add an e2e assertion for the authenticated login -> dashboard redirect
Build, 19 unit tests, and 3 e2e all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Establish the testing safety net for the web dashboard before refactoring.
- Vitest + React Testing Library + jsdom for unit/component tests
- MSW node server mocks the API; unhandled requests throw so tests can
never reach a real backend
- Playwright e2e boots the real Next app but intercepts every /api/v1 call
with shared fixtures, and mints a NextAuth session cookie to run authed
- Shared fixtures in test/fixtures.ts back both layers
- Seed tests: Button unit tests, MSW sanity checks, and a dashboard e2e
happy path plus an auth-guard redirect
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>