diff --git a/contracts/settings/v1/conformance.json b/contracts/settings/v1/conformance.json index 1a53a6f9..1fd60cbd 100644 --- a/contracts/settings/v1/conformance.json +++ b/contracts/settings/v1/conformance.json @@ -1,6 +1,6 @@ { "fixture_version": 1, - "manifest_revision": 3, + "manifest_revision": 4, "description": "Cross-platform conformance cases for settings resolution. Every case runs against the shipped manifest in this directory: definitions are referenced by key, never restated, so an expectation can only be satisfied by resolving the real contract. Each platform's resolver (Go in internal/settingsresolve, TypeScript in web/src/lib/settingsResolve.ts, Kotlin and Swift in the client repos) runs every case through a hand-written runner; a runner must fail on any fixture field it does not know, because schema drift in the fixture itself is drift. A case's constraint_bindings attach a constraint to a copy of a real definition so constraint semantics stay testable even while no shipped definition carries that constraint kind. In expected entries, constrained:true requires stored_value and constraint_kind to be present, and stored_value may be null to mean the authored value was JSON null.", "cases": [ { diff --git a/contracts/settings/v1/manifest.json b/contracts/settings/v1/manifest.json index 57743d3b..91445620 100644 --- a/contracts/settings/v1/manifest.json +++ b/contracts/settings/v1/manifest.json @@ -1,6 +1,6 @@ { "api_version": 1, - "revision": 3, + "revision": 4, "option_sets": { "playback_audio_languages": { "type": "language_tag", @@ -801,7 +801,7 @@ "nullable": true }, "default_value": null, - "platforms": ["web"], + "platforms": ["web", "ios", "tvos", "macos", "android", "android_tv"], "category": "appearance", "label": "Poster badges", "description": "Which badges appear on poster cards, and where.", diff --git a/internal/settingskeys/keys.go b/internal/settingskeys/keys.go index 01161a8f..d34c4b37 100644 --- a/internal/settingskeys/keys.go +++ b/internal/settingskeys/keys.go @@ -9,7 +9,7 @@ package settingskeys // Revision is the manifest revision these bindings were generated from. -const Revision = 3 +const Revision = 4 // Setting keys, one constant per definition. const ( diff --git a/web/src/lib/overlays/schema.ts b/web/src/lib/overlays/schema.ts index c63f8a5b..26b3376f 100644 --- a/web/src/lib/overlays/schema.ts +++ b/web/src/lib/overlays/schema.ts @@ -13,6 +13,22 @@ export function buildDefaultPrefs(): CardOverlayPrefs { return { version: 2, preset: "classic", order: [], items: buildItems(undefined) }; } +// Ids that are legal in the contract schema (card-overlays.json) but have no +// registry entry yet because no API field backs them. The web client neither +// renders nor edits these, but their stored config must survive a round-trip: +// the native clients' settings UIs can author them, and dropping them here +// would erase another client's preference on the next web save. Their bases +// mirror the native registries' defaults (ribbons: top-right, disabled). +const PASSTHROUGH_IDS = ["imdb_top_250", "rt_certified_fresh"] as const satisfies readonly OverlayId[]; +const PASSTHROUGH_BASE: OverlayItemConfig = { enabled: false, position: "top-right" }; + +function isKnownOverlayId(v: unknown): v is OverlayId { + return ( + typeof v === "string" && + (OVERLAY_MAP.has(v as OverlayId) || (PASSTHROUGH_IDS as readonly string[]).includes(v)) + ); +} + function isValidPosition(v: unknown): v is OverlayPosition { return typeof v === "string" && (OVERLAY_POSITIONS as readonly string[]).includes(v); } @@ -61,6 +77,12 @@ function buildItems( ? applyItemPatch(base, entry as Record) : base; } + for (const id of PASSTHROUGH_IDS) { + const entry = source?.[id]; + if (entry && typeof entry === "object") { + items[id] = applyItemPatch(PASSTHROUGH_BASE, entry as Record); + } + } return items; } @@ -75,11 +97,7 @@ function parseV2(parsed: Record): CardOverlayPrefs { return { version: 2, preset: isValidPreset(parsed.preset) ? parsed.preset : "classic", - order: Array.isArray(parsed.order) - ? (parsed.order as unknown[]).filter( - (id): id is OverlayId => typeof id === "string" && OVERLAY_MAP.has(id as OverlayId), - ) - : [], + order: Array.isArray(parsed.order) ? (parsed.order as unknown[]).filter(isKnownOverlayId) : [], items: buildItems(sourceItems), }; } diff --git a/web/src/lib/settingsConformance.json b/web/src/lib/settingsConformance.json index 1a53a6f9..1fd60cbd 100644 --- a/web/src/lib/settingsConformance.json +++ b/web/src/lib/settingsConformance.json @@ -1,6 +1,6 @@ { "fixture_version": 1, - "manifest_revision": 3, + "manifest_revision": 4, "description": "Cross-platform conformance cases for settings resolution. Every case runs against the shipped manifest in this directory: definitions are referenced by key, never restated, so an expectation can only be satisfied by resolving the real contract. Each platform's resolver (Go in internal/settingsresolve, TypeScript in web/src/lib/settingsResolve.ts, Kotlin and Swift in the client repos) runs every case through a hand-written runner; a runner must fail on any fixture field it does not know, because schema drift in the fixture itself is drift. A case's constraint_bindings attach a constraint to a copy of a real definition so constraint semantics stay testable even while no shipped definition carries that constraint kind. In expected entries, constrained:true requires stored_value and constraint_kind to be present, and stored_value may be null to mean the authored value was JSON null.", "cases": [ { diff --git a/web/src/lib/settingsContract.ts b/web/src/lib/settingsContract.ts index 11aab0a0..7849ede3 100644 --- a/web/src/lib/settingsContract.ts +++ b/web/src/lib/settingsContract.ts @@ -8,7 +8,7 @@ * is a manifest change plus a regeneration, never a hand-written key. */ -export const SETTINGS_REVISION = 3; +export const SETTINGS_REVISION = 4; export interface SettingSuggestedOption { value: string; @@ -934,7 +934,7 @@ export const SETTING_DEFINITIONS: Record = { label: "Poster badges", description: "Which badges appear on poster cards, and where.", category: "appearance", - platforms: ["web"], + platforms: ["web", "ios", "tvos", "macos", "android", "android_tv"], }, "ui.custom_css": { key: "ui.custom_css",