diff --git a/README.md b/README.md
index 23d4d476..41133d95 100644
--- a/README.md
+++ b/README.md
@@ -161,6 +161,7 @@ For a full theming guide with example themes and an AI prompt to generate your o
| `customize.websiteLogo` | `OCAP_CUSTOMIZE_WEBSITELOGO` | URL to your website logo | `""` |
| `customize.websiteLogoSize` | `OCAP_CUSTOMIZE_WEBSITELOGOSIZE` | Logo size | `32px` |
| `customize.disableKillCount` | `OCAP_CUSTOMIZE_DISABLEKILLCOUNT` | Hide kill counts in the UI | `false` |
+| `customize.hideMapFilters` | `OCAP_CUSTOMIZE_HIDEMAPFILTERS` | Hide the map filter dropdown on the recording list | `false` |
| `customize.headerTitle` | `OCAP_CUSTOMIZE_HEADERTITLE` | Custom header title | `""` |
| `customize.headerSubtitle` | `OCAP_CUSTOMIZE_HEADERSUBTITLE` | Custom header subtitle | `""` |
| `customize.cssOverrides` | `OCAP_CUSTOMIZE_CSSOVERRIDES` | CSS variable overrides (JSON object, see below) | `{}` |
diff --git a/internal/server/setting.go b/internal/server/setting.go
index 676a2ae1..b39214f9 100644
--- a/internal/server/setting.go
+++ b/internal/server/setting.go
@@ -47,6 +47,7 @@ type Customize struct {
WebsiteLogo string `json:"websiteLogo" yaml:"websiteLogo"`
WebsiteLogoSize string `json:"websiteLogoSize" yaml:"websiteLogoSize"`
DisableKillCount bool `json:"disableKillCount" yaml:"disableKillCount"`
+ HideMapFilters bool `json:"hideMapFilters" yaml:"hideMapFilters"`
HeaderTitle string `json:"headerTitle" yaml:"headerTitle"`
HeaderSubtitle string `json:"headerSubtitle" yaml:"headerSubtitle"`
PageTitle string `json:"pageTitle" yaml:"pageTitle"`
@@ -109,6 +110,7 @@ func NewSetting() (setting Setting, err error) {
viper.SetDefault("customize.websiteLogo", "")
viper.SetDefault("customize.websiteLogoSize", "32px")
viper.SetDefault("customize.disableKillCount", false)
+ viper.SetDefault("customize.hideMapFilters", false)
viper.SetDefault("customize.headerTitle", "")
viper.SetDefault("customize.headerSubtitle", "")
viper.SetDefault("customize.pageTitle", "")
diff --git a/internal/server/setting_test.go b/internal/server/setting_test.go
index dbf479d9..2a4fd92f 100644
--- a/internal/server/setting_test.go
+++ b/internal/server/setting_test.go
@@ -146,6 +146,7 @@ func TestNewSetting_ConfigFile(t *testing.T) {
assert.Empty(t, setting.Customize.WebsiteURL)
assert.Empty(t, setting.Customize.WebsiteLogo)
assert.False(t, setting.Customize.DisableKillCount)
+ assert.False(t, setting.Customize.HideMapFilters)
})
t.Run("customize values from config", func(t *testing.T) {
@@ -158,6 +159,7 @@ func TestNewSetting_ConfigFile(t *testing.T) {
"websiteLogo": "/logo.png",
"websiteLogoSize": "64px",
"disableKillCount": true,
+ "hideMapFilters": true,
"headerTitle": "My Community",
"headerSubtitle": "After Action Reviews"
}
@@ -175,6 +177,7 @@ func TestNewSetting_ConfigFile(t *testing.T) {
assert.Equal(t, "/logo.png", setting.Customize.WebsiteLogo)
assert.Equal(t, "64px", setting.Customize.WebsiteLogoSize)
assert.True(t, setting.Customize.DisableKillCount)
+ assert.True(t, setting.Customize.HideMapFilters)
assert.Equal(t, "My Community", setting.Customize.HeaderTitle)
assert.Equal(t, "After Action Reviews", setting.Customize.HeaderSubtitle)
})
diff --git a/setting.json.example b/setting.json.example
index 9b64c136..f8a1b900 100644
--- a/setting.json.example
+++ b/setting.json.example
@@ -14,6 +14,7 @@
"customize": {
"disableKillCount": false,
"enabled": false,
+ "hideMapFilters": false,
"headerSubtitle": "",
"headerTitle": "",
"pageTitle": "",
diff --git a/ui/src/data/apiClient.ts b/ui/src/data/apiClient.ts
index 03434901..fb626566 100644
--- a/ui/src/data/apiClient.ts
+++ b/ui/src/data/apiClient.ts
@@ -20,6 +20,7 @@ export interface CustomizeConfig {
websiteLogo?: string;
websiteLogoSize?: string;
disableKillCount?: boolean;
+ hideMapFilters?: boolean;
headerTitle?: string;
headerSubtitle?: string;
pageTitle?: string;
diff --git a/ui/src/hooks/__tests__/useCustomize.test.tsx b/ui/src/hooks/__tests__/useCustomize.test.tsx
index fc86958f..3363dbdb 100644
--- a/ui/src/hooks/__tests__/useCustomize.test.tsx
+++ b/ui/src/hooks/__tests__/useCustomize.test.tsx
@@ -149,6 +149,27 @@ describe("useCustomize", () => {
expect(parsed.websiteURL).toBeUndefined();
});
+ it("honors hideMapFilters even when customize is not enabled", async () => {
+ mockGetCustomize.mockResolvedValue({
+ enabled: false,
+ hideMapFilters: true,
+ websiteURL: "https://should-not-appear.com",
+ });
+
+ const { getByTestId } = render(() => (
+
+ {}} />
+
+ ));
+
+ await vi.waitFor(() => {
+ const parsed = JSON.parse(getByTestId("config").textContent || "{}") as CustomizeConfig;
+ expect(parsed.hideMapFilters).toBe(true);
+ });
+ const parsed = JSON.parse(getByTestId("config").textContent || "{}") as CustomizeConfig;
+ expect(parsed.websiteURL).toBeUndefined();
+ });
+
it("applies pageTitle to document.title and restores on unmount", async () => {
const originalTitle = document.title;
document.title = "OCAP2";
diff --git a/ui/src/hooks/useCustomize.tsx b/ui/src/hooks/useCustomize.tsx
index 8bbe692f..f62a8940 100644
--- a/ui/src/hooks/useCustomize.tsx
+++ b/ui/src/hooks/useCustomize.tsx
@@ -24,11 +24,13 @@ export function CustomizeProvider(props: {
const data = await api.getCustomize();
if (!mounted) return;
if (!data.enabled) {
- // disableKillCount is a privacy toggle, not a branding option, so
- // honor it even when customize itself is not enabled.
- if (data.disableKillCount) {
- setConfig({ disableKillCount: true });
- }
+ // disableKillCount is a privacy toggle and hideMapFilters is a layout
+ // preference — neither is a branding option, so honor them even when
+ // customize itself is not enabled.
+ const fallback: CustomizeConfig = {};
+ if (data.disableKillCount) fallback.disableKillCount = true;
+ if (data.hideMapFilters) fallback.hideMapFilters = true;
+ if (Object.keys(fallback).length > 0) setConfig(fallback);
return;
}
setConfig(data);
diff --git a/ui/src/pages/recording-selector/RecordingSelector.tsx b/ui/src/pages/recording-selector/RecordingSelector.tsx
index d3e79fbe..9333888d 100644
--- a/ui/src/pages/recording-selector/RecordingSelector.tsx
+++ b/ui/src/pages/recording-selector/RecordingSelector.tsx
@@ -49,6 +49,9 @@ export function RecordingSelector(): JSX.Element {
const worldDisplayName = (systemName: string) => worldNames().get(systemName) ?? systemName;
+ // Map filters are shown unless the instance opts out via customize.hideMapFilters
+ const showMapFilters = (): boolean => !customize().hideMapFilters;
+
let searchRef: HTMLInputElement | undefined;
let scrollRef: HTMLDivElement | undefined;
@@ -440,7 +443,7 @@ export function RecordingSelector(): JSX.Element {
{/* Map filter dropdown */}
- 1}>
+ 1}>
{
});
});
+ it("shows the map filter dropdown by default", async () => {
+ const { findByTestId, queryByTestId } = renderPage();
+ await findByTestId("recording-1");
+
+ expect(queryByTestId("map-filter-dropdown-trigger")).not.toBeNull();
+ });
+
+ it("hides the map filter dropdown when customize.hideMapFilters is set", async () => {
+ const recordingsFetch = globalThis.fetch;
+ globalThis.fetch = vi.fn().mockImplementation((input: RequestInfo | URL) => {
+ const url = typeof input === "string" ? input : input.toString();
+ if (url.includes("/customize")) {
+ return Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({ enabled: true, hideMapFilters: true }),
+ } as Response);
+ }
+ return (recordingsFetch as ReturnType)(input);
+ });
+
+ const { findByTestId, queryByTestId } = renderPage();
+ await findByTestId("recording-1");
+
+ await vi.waitFor(() => {
+ expect(queryByTestId("map-filter-dropdown-trigger")).toBeNull();
+ });
+ // Other filters are unaffected
+ expect(queryByTestId("search-input")).not.toBeNull();
+ });
+
// ── Clear filters ──
it("shows clear button when filter is active and clears on click", async () => {