refactor(settings): remove the preview-feature opt-ins

The per-account preview switch was kept for the topic overview pages,
which were dropped before they shipped. Its one flag was stored, served
and backed up, and read by nothing; the column-reader audit listed it as
a pending consumer that will not come.

Removed completely:

- `/api/settings/preview-features` (GET, PATCH) is retired: it answers
  410 Gone with `meta.errorCode: route.retired` and `removedIn: 1.46.0`,
  and the contract publishes it under Retired. The route, its validation
  schema, the `PreviewFeatures` OpenAPI schemas and the settings module
  are deleted.
- Migration 0485 drops `users.preview_features_json` under a bounded lock
  wait, with the retry marker.
- The backup no longer writes the field. A file from v1.44 or v1.45 that
  carries it still restores: the schema accepts it and no codec reads it,
  so nothing is refused or written for it. The backup-parity goldens
  lose the field and apply one setting fewer.
- The data-wipe plan entry and the sharing-surface actor entry go with
  the route (frozen count 273 -> 272).

No client build reads the route: neither iOS repository nor any local
checkout references it.
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 00:44:15 +02:00
1 parent f3b9329fb2
commit 2ae23f4ca8
26 files changed
+125 -437

No files matched your search

-1
View File
@@ -2660,7 +2660,6 @@
"src/lib/search/__tests__/match.test.ts": 0,
"src/lib/search/__tests__/search-records.test.ts": 0,
"src/lib/settings/__tests__/module-list-layout.test.ts": 0,
"src/lib/settings/__tests__/preview-features.test.ts": 0,
"src/lib/sharing/__tests__/account-access-parse.test.ts": 0,
"src/lib/sharing/__tests__/account-access.test.ts": 0,
"src/lib/sharing/__tests__/account-label.test.ts": 0,
@@ -124,9 +124,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -2566,9 +2563,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -63,9 +63,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -1725,9 +1722,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -332,7 +332,7 @@
"symptomDefinitions": 2,
"lifeEvents": 3
},
"accountSettingsApplied": 62,
"accountSettingsApplied": 61,
"skipped": {
"catalogueKeys": [],
"links": 0
@@ -332,7 +332,7 @@
"symptomDefinitions": 0,
"lifeEvents": 0
},
"accountSettingsApplied": 62,
"accountSettingsApplied": 61,
"skipped": {
"catalogueKeys": [],
"links": 0
@@ -2759,9 +2759,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -3029,9 +3026,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -378,7 +378,7 @@
"symptomDefinitions": 0,
"lifeEvents": 0
},
"accountSettingsApplied": 62,
"accountSettingsApplied": 61,
"skipped": {
"catalogueKeys": [
{
@@ -2254,9 +2254,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -2463,9 +2460,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -894,7 +894,6 @@
"documentsLayoutJson": null,
"moodTagLayoutJson": null,
"moduleListLayoutJson": null,
"previewFeaturesJson": null,
"activityGoalsJson": null,
"glucoseUnit": null,
"hasDiabetes": false,
@@ -1027,7 +1027,6 @@
"documentsLayoutJson": null,
"moodTagLayoutJson": null,
"moduleListLayoutJson": null,
"previewFeaturesJson": null,
"activityGoalsJson": null,
"glucoseUnit": null,
"hasDiabetes": false,
@@ -1247,7 +1246,6 @@
"moodTagLayoutJson": null,
"documentsLayoutJson": null,
"moduleListLayoutJson": null,
"previewFeaturesJson": null,
"activityGoalsJson": null,
"reportSelectionJson": null,
"globalExcludedInjectionSites": [],
@@ -2374,9 +2374,6 @@
"moduleListLayoutJson": {
"roundTrip": "rt-User-moduleListLayoutJson"
},
"previewFeaturesJson": {
"roundTrip": "rt-User-previewFeaturesJson"
},
"activityGoalsJson": {
"stepsPerMonth": 250000,
"workoutsPerWeek": 4
@@ -1,13 +1,10 @@
/**
* The two per-user Settings preferences that live outside the account
* payload, end to end against Postgres:
*
* - `/api/settings/module-list-layout`: defaults without a write, one list
* changed per PUT with preserve-when-absent fields and untouched neighbours,
* the multi-issue 422 for unknown values, and the column carrying only what
* was chosen.
* - `/api/settings/preview-features`: all off by default, PATCH keeps the
* flags it does not name, an unknown flag or an empty body is refused.
* The per-user list layout, a Settings preference that lives outside the
* account payload, end to end against Postgres:
* `/api/settings/module-list-layout` answers defaults without a write,
* changes one list per PUT with preserve-when-absent fields and untouched
* neighbours, refuses unknown values with the multi-issue 422, and stores
* only what was chosen.
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -173,51 +170,8 @@ describe("/api/settings/module-list-layout — integration", () => {
});
});
describe("/api/settings/preview-features — integration", () => {
const URL = "http://localhost/api/settings/preview-features";
it("is all off by default and keeps a flag once switched on", async () => {
const { userId } = await seedUser();
const { GET, PATCH } =
await import("@/app/api/settings/preview-features/route");
const before = await (GET as Handler)(new Request(URL));
expect(((await before.json()) as { data: unknown }).data).toEqual({
topics: false,
});
const on = await (PATCH as Handler)(json("PATCH", URL, { topics: true }));
expect(on.status).toBe(200);
expect(((await on.json()) as { data: unknown }).data).toEqual({
topics: true,
});
const after = await (GET as Handler)(new Request(URL));
expect(((await after.json()) as { data: unknown }).data).toEqual({
topics: true,
});
const row = await getPrismaClient().user.findUnique({
where: { id: userId },
select: { previewFeaturesJson: true },
});
expect(row?.previewFeaturesJson).toEqual({ topics: true });
});
it("refuses an unknown flag and an empty body", async () => {
await seedUser();
const { PATCH } = await import("@/app/api/settings/preview-features/route");
const unknown = await (PATCH as Handler)(
json("PATCH", URL, { topics: true, darkMagic: true }),
);
expect(unknown.status).toBe(422);
const empty = await (PATCH as Handler)(json("PATCH", URL, {}));
expect(empty.status).toBe(422);
});
});
describe("settings presentation follows the person, also while acting for a record", () => {
const LAYOUT = "http://localhost/api/settings/module-list-layout";
const PREVIEW = "http://localhost/api/settings/preview-features";
async function actingForAnotherRecord() {
const prisma = getPrismaClient();
@@ -251,10 +205,9 @@ describe("settings presentation follows the person, also while acting for a reco
return { ownerId: owner.id, actorId: actor.id };
}
it("reads and saves the actor's own layout and preview switch", async () => {
it("reads and saves the actor's own layout", async () => {
const { ownerId, actorId } = await actingForAnotherRecord();
const layout = await import("@/app/api/settings/module-list-layout/route");
const preview = await import("@/app/api/settings/preview-features/route");
expect((await (layout.GET as Handler)(new Request(LAYOUT))).status).toBe(
200,
@@ -263,20 +216,12 @@ describe("settings presentation follows the person, also while acting for a reco
json("PUT", LAYOUT, { version: 1, list: "labs", view: "cards" }),
);
expect(put.status).toBe(200);
expect((await (preview.GET as Handler)(new Request(PREVIEW))).status).toBe(
200,
);
const patch = await (preview.PATCH as Handler)(
json("PATCH", PREVIEW, { topics: true }),
);
expect(patch.status).toBe(200);
const rows = await getPrismaClient().user.findMany({
where: { id: { in: [ownerId, actorId] } },
select: {
id: true,
moduleListLayoutJson: true,
previewFeaturesJson: true,
},
});
const actor = rows.find((r) => r.id === actorId)!;
@@ -284,9 +229,7 @@ describe("settings presentation follows the person, also while acting for a reco
expect(actor.moduleListLayoutJson).toMatchObject({
labs: { view: "cards" },
});
expect(actor.previewFeaturesJson).toEqual({ topics: true });
expect(owner.moduleListLayoutJson).toBeNull();
expect(owner.previewFeaturesJson).toBeNull();
});
});
@@ -69,7 +69,6 @@ import * as medicationLayout from "@/app/api/medications/layout/route";
import * as moodTagLayout from "@/app/api/mood/tags/layout/route";
import * as documentsLayout from "@/app/api/documents/inbound/layout/route";
import * as moduleListLayout from "@/app/api/settings/module-list-layout/route";
import * as previewFeatures from "@/app/api/settings/preview-features/route";
import * as activityGoals from "@/app/api/activity/goals/route";
import * as healthKit from "@/app/api/integrations/healthkit/route";
import { POST as exportHealthRecord } from "@/app/api/export/health-record/route";
@@ -888,21 +887,6 @@ function entries(user: Pick<User, "id">): Entry[] {
},
expected: ["list", "alphaAsc"],
},
{
columns: ["previewFeaturesJson"],
write: {
handler: previewFeatures.PATCH,
method: "PATCH",
route: "/api/settings/preview-features",
body: { topics: true },
},
read: get(
previewFeatures.GET,
"/api/settings/preview-features",
"topics",
),
expected: true,
},
{
columns: ["activityGoalsJson"],
write: {