fix(coach,withings): drop legacy_form_total counter + add navigator.onLine pre-check

v1.4.47 W7 — the in-memory legacy_form_total counter from v1.4.25 W21 Fix-K was per-process and useless on multi-container deploys; the access-log warning already carries the signal it surfaced. Drop the counter + the admin-status field that exposed it.

v1.4.47 W8 — Coach send-message path pre-checks navigator.onLine so an airplane-mode user gets the offline-specific 'coach.network' copy immediately instead of waiting for the fetch to fail with a generic network error. Surfaces W12 M6 properly.
This commit is contained in:
Marc-André Bombeck committed 2026-05-22 08:49:44 +02:00
1 parent 3e0eb77b4d
commit 8d0ae98afd
5 files changed
+22 -81

No files matched your search

@@ -45,16 +45,9 @@ vi.mock("@/lib/jobs/worker-status", () => ({
}));
// v1.4.25 W21 Fix-K — `/api/admin/status` now reads the legacy-form
// counter from the Withings webhook module. Mock it at module boundary
// so the test fixture stays deterministic.
vi.mock("@/app/api/withings/webhook/route", () => ({
getLegacyFormTotal: vi.fn(() => 0),
}));
import { GET } from "../route";
import { prisma } from "@/lib/db";
import { requireAdmin, HttpError } from "@/lib/api-handler";
import { getLegacyFormTotal } from "@/app/api/withings/webhook/route";
const ADMIN_CTX = {
session: { id: "s1", expiresAt: new Date(Date.now() + 3_600_000) },
@@ -178,12 +171,4 @@ describe("GET /api/admin/status (integration health summary)", () => {
expect(tokenCallArgs).toEqual({ where: { revoked: false } });
});
it("surfaces the legacy Withings webhook counter on the counters block (Fix-K sec-M2)", async () => {
vi.mocked(getLegacyFormTotal).mockReturnValue(7);
const res = await GET();
const body = (await res.json()) as {
data: { counters: { withingsWebhookLegacyFormTotal: number } };
};
expect(body.data.counters.withingsWebhookLegacyFormTotal).toBe(7);
});
});
-7
View File
@@ -3,7 +3,6 @@ import { apiHandler, requireAdmin } from "@/lib/api-handler";
import { apiSuccess } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { getWorkerStatus } from "@/lib/jobs/worker-status";
import { getLegacyFormTotal } from "@/app/api/withings/webhook/route";
import { readFileSync } from "node:fs";
// Build timestamp (set at build time via next.config)
@@ -92,11 +91,5 @@ export const GET = apiHandler(async () => {
webPush: webPushConfigured ? { configured: true } : null,
bugReport: bugReportConfigured ? { configured: true } : null,
},
// v1.4.25 W21 Fix-K — in-memory counters that the release-gate can
// read to confirm "legacy form usage trending toward zero" before
// the v1.4.27 cut of the legacy Withings webhook route.
counters: {
withingsWebhookLegacyFormTotal: getLegacyFormTotal(),
},
});
});
@@ -45,13 +45,7 @@ vi.mock("@/lib/logging/context", () => ({
})),
}));
import {
POST,
GET,
HEAD,
getLegacyFormTotal,
__resetLegacyFormTotalForTests,
} from "../route";
import { POST, GET, HEAD } from "../route";
import { prisma } from "@/lib/db";
import { syncUserMeasurements } from "@/lib/withings/sync";
import { syncUserActivity } from "@/lib/withings/sync-activity";
@@ -174,7 +168,6 @@ describe("POST /api/withings/webhook", () => {
});
it("legacy ?secret=… query path still authorises and emits a Wide Event warning with the migration URL (Fix-K sec-M2)", async () => {
__resetLegacyFormTotalForTests();
const addWarning = vi.fn();
vi.mocked(getEvent).mockReturnValue({
setAuth: vi.fn(),
@@ -198,25 +191,6 @@ describe("POST /api/withings/webhook", () => {
expect(addWarning).toHaveBeenCalledWith(
expect.stringContaining("/api/withings/webhook/[token]"),
);
// Fix-K sec-M2: in-memory counter increments on every legacy
// ?secret=… call so the release-gate can watch usage trend to zero.
expect(getLegacyFormTotal()).toBe(1);
});
it("counter stays at zero when the secret arrives via the header (Fix-K sec-M2)", async () => {
__resetLegacyFormTotalForTests();
vi.mocked(prisma.withingsConnection.findFirst).mockResolvedValueOnce({
userId: "user-h",
withingsUserId: "wu-h",
} as never);
await POST(
jsonRequest(
{ userid: "wu-h" },
{ "x-withings-webhook-secret": "test-secret" },
),
);
expect(getLegacyFormTotal()).toBe(0);
});
it("accepts form-encoded payloads (Withings legacy delivery format)", async () => {
+7 -32
View File
@@ -28,41 +28,17 @@ import {
* while users re-subscribe (which also happens organically when they
* reconnect for the new `user.activity` OAuth scope shipped in W5d).
*
* Removal target: v1.4.27 (after all live subscriptions have rotated).
* Removal target: a future release once all live subscriptions have
* rotated to the path-segment form. The warning is still emitted on the
* legacy path so an operator watching the access log knows what to
* migrate to.
*
* v1.4.25 W21 Fix-K — added an in-memory `withings.webhook.legacy_form_total`
* counter the `/api/admin/status` endpoint surfaces so the release-gate
* can read "legacy form usage trending toward zero" before the v1.4.27
* cut. The warning text now includes the re-subscription URL so anyone
* watching the access log knows what to migrate to.
* v1.4.47 — dropped the in-memory `legacy_form_total` counter (was per-
* process, useless on a multi-container deploy; the access-log warning
* already covers the signal it provided).
*/
const MIGRATION_URL = "/api/withings/webhook/[token]" as const;
// In-memory counter — resets on every deploy / cold-start. That is the
// intended granularity: we want to know "how many legacy-form requests
// did this process serve since it came up", not a forever-running
// cumulative metric (which would belong in Prisma).
const counters = {
legacy_form_total: 0,
};
/**
* Read the legacy-form counter. Exported so `/api/admin/status` can
* surface it without a backchannel.
*/
export function getLegacyFormTotal(): number {
return counters.legacy_form_total;
}
/**
* Reset the legacy-form counter. Test-only utility; production code
* does not call this. Documented so a future smoke test can wire up
* a deterministic "counter was 0 before the call" assertion.
*/
export function __resetLegacyFormTotalForTests(): void {
counters.legacy_form_total = 0;
}
function hasValidWebhookSecret(
request: NextRequest,
): Promise<{ ok: boolean; via: "header" | "query" | "none" }> {
@@ -89,7 +65,6 @@ async function checkAndWarn(
return NextResponse.json({ status: "unauthorized" }, { status: 401 });
}
if (auth.via === "query") {
counters.legacy_form_total += 1;
getEvent()?.addWarning(
`withings webhook secret received via legacy URL query — migrate to ${MIGRATION_URL} (re-subscribe with the path-segment token form)`,
);
@@ -327,6 +327,20 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
errorCode: null,
});
// v1.4.47 W8 — pre-check navigator.onLine so an airplane-mode
// user gets the offline-specific copy immediately rather than
// waiting for the fetch to fail with a generic network error.
if (typeof navigator !== "undefined" && navigator.onLine === false) {
setStreaming({
content: "",
metricSource: null,
inProgress: false,
messageId: null,
errorCode: "coach.network",
});
return;
}
let response: Response;
try {
response = await fetch("/api/insights/chat", {