diff --git a/.tmp-gradle-run/escalated/hsperfdata_Admin/25704 b/.tmp-gradle-run/escalated/hsperfdata_Admin/25704 deleted file mode 100644 index 92ddd7b..0000000 Binary files a/.tmp-gradle-run/escalated/hsperfdata_Admin/25704 and /dev/null differ diff --git a/.tmp-gradle-run/escalated/kotlin-compiler-in-emby-14545020421804070614.alive b/.tmp-gradle-run/escalated/kotlin-compiler-in-emby-14545020421804070614.alive deleted file mode 100644 index e69de29..0000000 diff --git a/admin-ui/src/api/types.ts b/admin-ui/src/api/types.ts index 9091beb..0e021a9 100644 --- a/admin-ui/src/api/types.ts +++ b/admin-ui/src/api/types.ts @@ -211,6 +211,21 @@ export interface FeaturePolicy { updatedAt?: string; canRollback: boolean; features: Feature[] | null; + configuration: ConfigurationValue[] | null; +} + +export interface ConfigurationValue { + key: string; + name: string; + description: string; + type: 'boolean' | 'integer' | 'enum' | 'json'; + scopes: string[]; + default: unknown; + options?: string[]; + min?: number; + max?: number; + value: unknown; + source: string; } export interface AdminStatus { diff --git a/admin-ui/src/nav.ts b/admin-ui/src/nav.ts index 33171a0..256ad5e 100644 --- a/admin-ui/src/nav.ts +++ b/admin-ui/src/nav.ts @@ -219,9 +219,9 @@ export const nav: NavGroup[] = [ { id: 'features', path: '/admin/features', - label: 'Features', - title: 'Features', - intro: 'Roll out, stop and recover optional behaviour with no app release.', + label: 'Client configuration', + title: 'Client configuration', + intro: 'Control everything the thin TV client renders, without releasing an APK.', icon: 'sliders', }, { diff --git a/admin-ui/src/pages/Features.tsx b/admin-ui/src/pages/Features.tsx index f2ee4ec..756f354 100644 --- a/admin-ui/src/pages/Features.tsx +++ b/admin-ui/src/pages/Features.tsx @@ -36,6 +36,7 @@ export function FeaturesPage() { const policy = status?.features; const features = policy?.features ?? []; + const configuration = policy?.configuration ?? []; const clients = status?.clients ?? []; const revision = policy?.revision ?? 0; @@ -64,12 +65,16 @@ export function FeaturesPage() { ), [key]: enabled, }); + const valuesFor = (key: string, value: unknown) => ({ + ...Object.fromEntries(configuration.map((item) => [item.key, item.value])), + [key]: value, + }); return ( <> @@ -77,6 +82,18 @@ export function FeaturesPage() { ) : ( <> + +
+ Hero sources and pinned overrides + For You and recommendation pools + Contextual row engagement signals + Client versions and capabilities +
+
+ +
+ {configuration.map((item) => { + const value = item.value; + return ( +
+
+ {item.name} +
{item.description} · {item.scopes.join(', ')}
+ {item.key} +
+ {item.type === 'boolean' ? ( + void run(item.key, async () => { + await wrap(() => api.post('/admin/api/features', { action: 'save', expectedRevision: revision, values: valuesFor(item.key, next), overrides: Object.fromEntries(features.filter((f) => f.source === 'override').map((f) => [f.key, f.enabled])) }), 'Configuration saved.'); + await reload(); + })} + /> + ) : item.type === 'enum' ? ( + + ) : item.type === 'json' ? ( +