mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
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);
|
||||
});
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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", {
|
||||
|
||||
Reference in new issue
Block a user