fix(recovery): claim no Oura permission the server cannot prove

The empty recovery note told every connected Oura account without a
recorded permission gap that its permission covers resilience. The
server cannot know that: the scope note is cleared on each reconnect
and written only by a sync that reached Oura, and a missing health
block says nothing either way. The note now asks for a reconnect only
when the gap is recorded and otherwise says only that no level has
arrived, pointing at the Oura card. It also waits for the connection
status instead of showing the general note first and swapping it.

Refs #1204
This commit is contained in:
Marc-André Bombeck committed 2026-10-19 18:31:45 +02:00
1 parent 8c745fca8d
commit c016ec8aec
9 files changed
+50 -24

No files matched your search

@@ -9,10 +9,10 @@ import type { DataSummary } from "@/lib/analytics/trends";
/**
* The recovery page's empty note says what is missing for THIS account. An
* Oura ring fills the page through its resilience level alone, so a connected
* ring with an empty page is either a grant that leaves out the heart-health
* data the level sits behind, or Oura not having sent a level yet. The
* connection facts come from the server's integrations read; the page only
* picks the sentence.
* ring with an empty page asks for a reconnect only when the server recorded
* that the grant leaves resilience out, and otherwise says only that no
* level has arrived. The connection facts come from the server's
* integrations read; the page only picks the sentence, and waits for it.
*/
vi.mock("next/dynamic", () => ({
@@ -57,14 +57,19 @@ const OURA_SUMMARIES = {
HRV_RMSSD: summary(30),
};
function render(oura: Record<string, unknown> | null) {
function render(
oura: Record<string, unknown> | null,
opts: { statusLoaded?: boolean } = {},
) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
queryClient.setQueryData(queryKeys.integrationsStatus(), {
threshold: 3,
integrations: oura ? [{ integration: "oura", ...oura }] : [],
});
if (opts.statusLoaded !== false) {
queryClient.setQueryData(queryKeys.integrationsStatus(), {
threshold: 3,
integrations: oura ? [{ integration: "oura", ...oura }] : [],
});
}
return renderToStaticMarkup(
<QueryClientProvider client={queryClient}>
<I18nProvider initialLocale="en">
@@ -104,16 +109,34 @@ describe("<RecoverySection> empty note", () => {
expect(html).toContain('href="/settings/integrations"');
});
it("says Oura has sent no level yet when the grant covers it", () => {
it("says only that no level has arrived when no withheld permission is recorded", () => {
const html = render({
connected: true,
health: health([{ item: "spo2", cause: "permission" }]),
});
expect(html).toContain('data-empty-case="oura-no-resilience"');
expect(html).toContain("has not sent a resilience level");
expect(html).toContain("no resilience level has arrived");
// The server cannot prove the grant covers resilience, so the note
// never says it does.
expect(html).not.toContain("permission covers");
expect(html).not.toContain('href="/settings/integrations"');
});
it("stays neutral when the connection carries no health block", () => {
const html = render({ connected: true });
expect(html).toContain('data-empty-case="oura-no-resilience"');
expect(html).not.toContain("permission covers");
});
it("shows no note until the connection status has loaded", () => {
const html = render(
{ connected: true, health: health([]) },
{ statusLoaded: false },
);
expect(html).toContain('data-slot="recovery-loading"');
expect(html).not.toContain("data-empty-case");
});
it("keeps the general note for an account without Oura", () => {
const html = render(null);
expect(html).toContain('data-empty-case="general"');
@@ -49,11 +49,12 @@ interface BlockGroup {
/**
* Why this account's recovery page is empty, from the server's view of its
* Oura connection. An Oura ring feeds this page through its resilience level
* alone, so a connected ring with an empty page has exactly two causes: the
* grant does not cover the heart-health data the level sits behind (the
* server records that per collection and publishes it as an unavailable
* item), or Oura has not sent a level for this account. Everyone else gets
* the general note naming what fills the page.
* alone. When the server records that the grant leaves resilience out, the
* note asks for one reconnect. Otherwise it says only that no level has
* arrived: the server cannot tell "the grant covers it" from "not recorded
* yet" (the scope note is cleared on every reconnect and written only by a
* sync that reached Oura), so the note never claims coverage. Everyone else
* gets the general note naming what fills the page.
*/
type RecoveryEmptyCase = "general" | "oura-permission" | "oura-no-resilience";
@@ -222,7 +223,9 @@ export function RecoverySection() {
);
const emptyCase = recoveryEmptyCase(pickStatus(statuses.data, "oura"));
if (isLoading || summaries == null) {
// The empty note waits for the connection status, so it never reads as
// the general note first and then changes under the reader.
if (isLoading || summaries == null || (!hasAny && statuses.isPending)) {
return (
<div className="space-y-3" data-slot="recovery-loading">
<ChartSkeleton />