mirror of
https://github.com/Chevron7Locked/kima-hub.git
synced 2026-06-19 07:37:17 +00:00
fix: onboarding validation rejects disabled integrations, hide audiobooks when disabled (#162)
- Backend: conditional URL validation -- skip format check when integration is disabled - Frontend: send clean payloads for disabled integrations during onboarding - Sidebar: hide Audiobooks nav item when Audiobookshelf is not enabled - Feature detection: expose audiobookshelfEnabled flag via /system/features - Audiobooks: use refetchQueries instead of invalidateQueries after sync for immediate UI update
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kima-backend",
|
||||
"version": "1.7.4",
|
||||
"version": "1.7.5",
|
||||
"description": "Kima backend API server",
|
||||
"license": "GPL-3.0",
|
||||
"repository": {
|
||||
|
||||
@@ -18,16 +18,22 @@ const registerSchema = z.object({
|
||||
});
|
||||
|
||||
const lidarrConfigSchema = z.object({
|
||||
url: z.string().url().optional().or(z.literal("")),
|
||||
url: z.string().optional().or(z.literal("")),
|
||||
apiKey: z.string().optional().or(z.literal("")),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
}).refine(
|
||||
(data) => !data.enabled || !data.url || data.url === "" || z.string().url().safeParse(data.url).success,
|
||||
{ message: "Invalid url", path: ["url"] },
|
||||
);
|
||||
|
||||
const audiobookshelfConfigSchema = z.object({
|
||||
url: z.string().url().optional().or(z.literal("")),
|
||||
url: z.string().optional().or(z.literal("")),
|
||||
apiKey: z.string().optional().or(z.literal("")),
|
||||
enabled: z.boolean(),
|
||||
});
|
||||
}).refine(
|
||||
(data) => !data.enabled || !data.url || data.url === "" || z.string().url().safeParse(data.url).success,
|
||||
{ message: "Invalid url", path: ["url"] },
|
||||
);
|
||||
|
||||
const soulseekConfigSchema = z.object({
|
||||
username: z.string().optional().or(z.literal("")),
|
||||
|
||||
@@ -10,6 +10,7 @@ const CLAP_ANALYZER_PATH = "/app/audio-analyzer-clap/analyzer.py";
|
||||
export interface AvailableFeatures {
|
||||
musicCNN: boolean;
|
||||
vibeEmbeddings: boolean;
|
||||
audiobookshelfEnabled: boolean;
|
||||
}
|
||||
|
||||
const HEARTBEAT_TTL = 300000; // 5 minutes
|
||||
@@ -25,16 +26,17 @@ class FeatureDetectionService {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
const [musicCNN, vibeEmbeddings] = await Promise.all([
|
||||
const [musicCNN, vibeEmbeddings, audiobookshelfEnabled] = await Promise.all([
|
||||
this.checkMusicCNN(),
|
||||
this.checkCLAP(),
|
||||
this.checkAudiobookshelf(),
|
||||
]);
|
||||
|
||||
this.cache = { musicCNN, vibeEmbeddings };
|
||||
this.cache = { musicCNN, vibeEmbeddings, audiobookshelfEnabled };
|
||||
this.lastCheck = now;
|
||||
|
||||
logger.debug(
|
||||
`[FEATURE-DETECTION] Features: musicCNN=${musicCNN}, vibeEmbeddings=${vibeEmbeddings}`
|
||||
`[FEATURE-DETECTION] Features: musicCNN=${musicCNN}, vibeEmbeddings=${vibeEmbeddings}, audiobookshelf=${audiobookshelfEnabled}`
|
||||
);
|
||||
|
||||
return this.cache;
|
||||
@@ -94,6 +96,19 @@ class FeatureDetectionService {
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAudiobookshelf(): Promise<boolean> {
|
||||
try {
|
||||
const settings = await prisma.systemSettings.findUnique({
|
||||
where: { id: "default" },
|
||||
select: { audiobookshelfEnabled: true },
|
||||
});
|
||||
return settings?.audiobookshelfEnabled ?? false;
|
||||
} catch (error) {
|
||||
logger.error("[FEATURE-DETECTION] Error checking Audiobookshelf:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
invalidateCache(): void {
|
||||
this.cache = null;
|
||||
this.lastCheck = 0;
|
||||
|
||||
@@ -247,7 +247,7 @@ export default function AudiobooksPage() {
|
||||
if (result?.failed) parts.push(`${result.failed} failed`);
|
||||
if (result?.skipped) parts.push(`${result.skipped} skipped`);
|
||||
toast.success(parts.join(", "));
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.audiobooks() });
|
||||
await queryClient.refetchQueries({ queryKey: queryKeys.audiobooks() });
|
||||
} catch {
|
||||
toast.error("Audiobook sync failed");
|
||||
} finally {
|
||||
|
||||
@@ -159,10 +159,11 @@ export default function OnboardingPage() {
|
||||
try {
|
||||
if (step === 2) {
|
||||
// Save all integration configs and complete onboarding
|
||||
// Only send field values for enabled integrations; disabled ones get clean payloads
|
||||
await Promise.all([
|
||||
api.post("/onboarding/lidarr", lidarr),
|
||||
api.post("/onboarding/audiobookshelf", audiobookshelf),
|
||||
api.post("/onboarding/soulseek", soulseek),
|
||||
api.post("/onboarding/lidarr", lidarr.enabled ? lidarr : { url: "", apiKey: "", enabled: false }),
|
||||
api.post("/onboarding/audiobookshelf", audiobookshelf.enabled ? audiobookshelf : { url: "", apiKey: "", enabled: false }),
|
||||
api.post("/onboarding/soulseek", soulseek.enabled ? soulseek : { username: "", password: "", enabled: false }),
|
||||
]);
|
||||
await api.post("/onboarding/complete");
|
||||
router.push("/sync");
|
||||
|
||||
@@ -12,18 +12,19 @@ import { queryKeys } from "@/hooks/useQueries";
|
||||
import { useAudioState } from "@/lib/audio-state-context";
|
||||
import { useIsMobile, useIsTablet } from "@/hooks/useMediaQuery";
|
||||
import { useToast } from "@/lib/toast-context";
|
||||
import { useFeatures } from "@/lib/features-context";
|
||||
import Image from "next/image";
|
||||
import { MobileSidebar } from "./MobileSidebar";
|
||||
|
||||
const navigation = [
|
||||
{ name: "Collection", href: "/collection" },
|
||||
{ name: "Radio", href: "/radio" },
|
||||
{ name: "Discovery", href: "/discover" },
|
||||
{ name: "Vibe", href: "/vibe" },
|
||||
{ name: "Audiobooks", href: "/audiobooks" },
|
||||
{ name: "Podcasts", href: "/podcasts" },
|
||||
{ name: "Browse", href: "/browse/playlists" },
|
||||
] as const;
|
||||
const allNavigation = [
|
||||
{ name: "Collection", href: "/collection", feature: null },
|
||||
{ name: "Radio", href: "/radio", feature: null },
|
||||
{ name: "Discovery", href: "/discover", feature: null },
|
||||
{ name: "Vibe", href: "/vibe", feature: null },
|
||||
{ name: "Audiobooks", href: "/audiobooks", feature: "audiobookshelfEnabled" as const },
|
||||
{ name: "Podcasts", href: "/podcasts", feature: null },
|
||||
{ name: "Browse", href: "/browse/playlists", feature: null },
|
||||
];
|
||||
|
||||
interface Playlist {
|
||||
id: string;
|
||||
@@ -42,6 +43,7 @@ export function Sidebar() {
|
||||
const { toast } = useToast();
|
||||
const { currentTrack, currentAudiobook, currentPodcast, playbackType } =
|
||||
useAudioState();
|
||||
const { audiobookshelfEnabled } = useFeatures();
|
||||
const isMobile = useIsMobile();
|
||||
const isTablet = useIsTablet();
|
||||
const isMobileOrTablet = isMobile || isTablet;
|
||||
@@ -267,7 +269,9 @@ export function Sidebar() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{navigation.map((item, index) => {
|
||||
{allNavigation.filter((item) =>
|
||||
item.feature === null || (item.feature === "audiobookshelfEnabled" && audiobookshelfEnabled)
|
||||
).map((item, index) => {
|
||||
const isActive = pathname === item.href;
|
||||
|
||||
return (
|
||||
|
||||
+2
-2
@@ -742,8 +742,8 @@ class ApiClient {
|
||||
}
|
||||
|
||||
// System Features
|
||||
async getFeatures(): Promise<{ musicCNN: boolean; vibeEmbeddings: boolean }> {
|
||||
return this.request<{ musicCNN: boolean; vibeEmbeddings: boolean }>(
|
||||
async getFeatures(): Promise<{ musicCNN: boolean; vibeEmbeddings: boolean; audiobookshelfEnabled: boolean }> {
|
||||
return this.request<{ musicCNN: boolean; vibeEmbeddings: boolean; audiobookshelfEnabled: boolean }>(
|
||||
"/system/features"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ import { useAuth } from "./auth-context";
|
||||
interface FeaturesState {
|
||||
musicCNN: boolean;
|
||||
vibeEmbeddings: boolean;
|
||||
audiobookshelfEnabled: boolean;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const defaultState: FeaturesState = {
|
||||
musicCNN: false,
|
||||
vibeEmbeddings: false,
|
||||
audiobookshelfEnabled: false,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
@@ -31,6 +33,7 @@ export function FeaturesProvider({ children }: { children: ReactNode }) {
|
||||
setState({
|
||||
musicCNN: features.musicCNN,
|
||||
vibeEmbeddings: features.vibeEmbeddings,
|
||||
audiobookshelfEnabled: features.audiobookshelfEnabled,
|
||||
loading: false,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kima-frontend",
|
||||
"version": "1.7.4",
|
||||
"version": "1.7.5",
|
||||
"description": "Kima web frontend",
|
||||
"license": "GPL-3.0",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user