diff --git a/src/components/resources/MusicMoodFilter.tsx b/src/components/resources/MusicMoodFilter.tsx
index 167c7ad0..77c37053 100644
--- a/src/components/resources/MusicMoodFilter.tsx
+++ b/src/components/resources/MusicMoodFilter.tsx
@@ -2,16 +2,20 @@ import { useState, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ScrollArea } from '@/components/ui/scroll-area';
-import { IconSearch, IconX, IconMoodHappy } from '@tabler/icons-react';
+import { Skeleton } from '@/components/ui/skeleton';
+import { IconSearch, IconX, IconMoodHappy, IconAlertTriangle } from '@tabler/icons-react';
import { MusicMood } from '@/types/music';
interface MusicMoodFilterProps {
selectedMoods: string[];
onMoodChange: (moods: string[]) => void;
moodsData: MusicMood[];
+ isLoading?: boolean;
+ error?: string | null;
+ onRetry?: () => void;
}
-const MusicMoodFilter = ({ selectedMoods, onMoodChange, moodsData }: MusicMoodFilterProps) => {
+const MusicMoodFilter = ({ selectedMoods, onMoodChange, moodsData, isLoading = false, error = null, onRetry }: MusicMoodFilterProps) => {
const [moodSearch, setMoodSearch] = useState('');
const allMoods = useMemo(() => {
@@ -41,7 +45,10 @@ const MusicMoodFilter = ({ selectedMoods, onMoodChange, moodsData }: MusicMoodFi
setMoodSearch('');
};
- if (allMoods.length === 0) return null;
+ // The panel used to return null whenever the master mood list came back empty,
+ // so a failed /music_moods request hid the whole filter with no explanation.
+ const hasMoods = allMoods.length > 0;
+ const showBody = !isLoading && !error && hasMoods;
return (
@@ -58,8 +65,9 @@ const MusicMoodFilter = ({ selectedMoods, onMoodChange, moodsData }: MusicMoodFi
value={moodSearch}
onChange={(e) => setMoodSearch(e.target.value)}
className="pl-8 h-8 text-sm "
+ disabled={!showBody}
/>
- {moodSearch && (
+ {moodSearch && showBody && (
@@ -522,6 +538,9 @@ const ResourcesHub = () => {
selectedMoods={selectedMoods}
onMoodChange={setSelectedMoods}
moodsData={musicMoodsData}
+ isLoading={moodsStatus === 'loading' || moodsStatus === 'idle'}
+ error={moodsStatus === 'error' ? 'Mood filters could not be loaded.' : null}
+ onRetry={() => setMoodsStatus('idle')}
/>
diff --git a/workers/api-router/src/index.ts b/workers/api-router/src/index.ts
index 29d4f2a4..f41059cb 100644
--- a/workers/api-router/src/index.ts
+++ b/workers/api-router/src/index.ts
@@ -49,7 +49,15 @@ const E3_ROUTER_CLIENT_ID = "api-router";
type Group = "assets" | "resources";
-const RESOURCE_PREFIXES = ["/categories", "/category", "/all", "/fonts", "/mcicons"];
+const RESOURCE_PREFIXES = ["/categories", "/category", "/all", "/fonts", "/mcicons", "/music_moods"];
+
+// JSON API paths (as opposed to file paths). A 404 here means the origin does not
+// implement the route at all, so the next origin should be tried instead of
+// returning the 404 to the client — that is how /music_moods silently disappeared.
+const JSON_API_PATHS = ["/all", "/categories", "/mcicons", "/fonts", "/music_moods"];
+
+const isJsonApiPath = (pathname: string): boolean =>
+ JSON_API_PATHS.includes(pathname) || pathname.startsWith("/category/");
// Per-origin passive health: an origin that fails is skipped for a cooldown, then retried.
const originDownUntil = new Map();
@@ -104,7 +112,7 @@ const classify = (pathname: string): Group =>
: "assets";
// Read-only endpoints a partner-scoped key (`assets:read`) may request.
-const PARTNER_READ_PREFIXES = ["/all", "/categories", "/category", "/mcicons", "/fonts"];
+const PARTNER_READ_PREFIXES = ["/all", "/categories", "/category", "/mcicons", "/fonts", "/music_moods"];
const isPartnerReadPath = (pathname: string): boolean =>
PARTNER_READ_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`));
@@ -255,6 +263,12 @@ async function fetchUpstream(env: Env, path: string): Promise<{ ok: boolean; sta
last = { ok: false, status: upstream.status, text: "" };
continue;
}
+ if (upstream.status === 404 && !isLast && isJsonApiPath(path)) {
+ // Healthy origin that does not implement this JSON route (e.g. an older
+ // e3 build): ask the next origin rather than handing the 404 to the client.
+ last = { ok: false, status: 404, text: "" };
+ continue;
+ }
markOriginUp(origin);
return { ok: upstream.ok, status: upstream.status, text: await upstream.text() };
} catch {
@@ -403,6 +417,12 @@ export default {
continue;
}
+ if (upstream.status === 404 && !isLast && isJsonApiPath(url.pathname)) {
+ console.warn(`${origin} does not implement ${url.pathname} (404); trying next origin`);
+ markOriginUp(origin);
+ continue;
+ }
+
markOriginUp(origin);
if (!isBodyless && upstream.status < 400) ctx.waitUntil(invalidateVersion(env));
return applyCors(upstream, cors, origin === primary ? "e3" : "fallback");