From 60892c12e7448f7d4ca9bd2a6778eb78681be2df Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 18 Mar 2026 13:50:18 -0500 Subject: [PATCH] fix: multi-container docker-compose proxy, audiobookshelf sync bugs, add sync button Fixes #158 -- fresh docker-compose install shows login instead of setup wizard because Next.js rewrites were compiled with 127.0.0.1:3006 baked in. Added NEXT_PUBLIC_BACKEND_URL as a Dockerfile build arg, defaulting to http://backend:3006 in docker-compose.yml so the frontend container proxies to the backend service correctly. Audiobookshelf sync fixes: - syncAudiobook returns boolean; skipped books no longer count as synced - Sync notification now surfaces failed/skipped counts - downloadCover has 10s fetch timeout (was unbounded) - book.size changed to book.media?.size for audio-only size Added sync button on audiobooks page so users can trigger a sync without going through settings or the enrichment system. Bump to v1.7.2 --- CHANGELOG.md | 14 ++++++++++ README.md | 2 +- backend/package.json | 2 +- backend/src/routes/audiobooks.ts | 6 ++--- backend/src/services/audiobookCache.ts | 36 +++++++++++++------------- docker-compose.yml | 3 +++ frontend/Dockerfile | 4 ++- frontend/app/audiobooks/page.tsx | 34 +++++++++++++++++++++++- frontend/package.json | 2 +- 9 files changed, 77 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fd14c6..e34b3ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to Kima will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.7.2] - 2026-03-18 + +### Fixed + +- **Multi-container docker-compose: fresh install shows login instead of setup wizard (#158)**: Next.js rewrites were compiled at build time with `127.0.0.1:3006` baked in because `NEXT_PUBLIC_BACKEND_URL` was never passed as a build arg. In multi-container mode the frontend container can't reach `127.0.0.1:3006` (that's the backend container). Fix: added `NEXT_PUBLIC_BACKEND_URL` as a Dockerfile build arg, defaulting to `http://backend:3006` in docker-compose.yml. The reporter's CORS issue was a secondary symptom -- once the proxy works, all requests are same-origin. +- **Audiobookshelf sync silently skips books**: Books with no title returned early without throwing, but `syncAll` counted them as synced. Now `syncAudiobook` returns a boolean; skipped books increment `result.skipped` instead of `result.synced`. +- **Audiobookshelf sync notification hides failures**: The "Synced N audiobooks" notification now includes failed/skipped counts when non-zero. +- **Audiobookshelf cover download can hang indefinitely**: `downloadCover()` had no fetch timeout. Added `AbortSignal.timeout(10_000)` to prevent a single slow cover from stalling the entire sync. +- **Audiobookshelf book size includes non-audio files**: Changed `book.size` to `book.media?.size` for audio-only size. + +### Added + +- **Sync button on audiobooks page**: Users can now sync audiobooks directly from the audiobooks page without going through settings or triggering a full enrichment cycle. Shows synced/failed/skipped counts in a toast and refreshes the grid automatically. + ## [1.7.1] - 2026-03-17 ### Fixed diff --git a/README.md b/README.md index 788739d..3a4f9de 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,7 @@ Production-ready releases. Updated when new stable versions are released. ```bash docker pull chevron7locked/kima:latest # or specific version -docker pull chevron7locked/kima:v1.7.1 +docker pull chevron7locked/kima:v1.7.2 ``` ### 🔴 Nightly (Development) diff --git a/backend/package.json b/backend/package.json index b168156..e320e4b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "kima-backend", - "version": "1.7.1", + "version": "1.7.2", "description": "Kima backend API server", "license": "GPL-3.0", "repository": { diff --git a/backend/src/routes/audiobooks.ts b/backend/src/routes/audiobooks.ts index b5ebd44..684f96c 100644 --- a/backend/src/routes/audiobooks.ts +++ b/backend/src/routes/audiobooks.ts @@ -7,7 +7,7 @@ import { audiobookshelfService } from "../services/audiobookshelf"; import { audiobookCacheService } from "../services/audiobookCache"; import { prisma } from "../utils/db"; import { requireAuthOrToken } from "../middleware/auth"; -import { imageLimiter, apiLimiter } from "../middleware/rateLimiter"; +import { apiLimiter } from "../middleware/rateLimiter"; import { getSystemSettings } from "../utils/systemSettings"; import { notificationService } from "../services/notificationService"; import { config } from "../config"; @@ -96,7 +96,7 @@ router.post("/sync", requireAuthOrToken, apiLimiter, async (req, res) => { await notificationService.notifySystem( req.user.id, "Audiobook Sync Complete", - `Synced ${result.synced || 0} audiobooks (${seriesCount} with series)` + `Synced ${result.synced || 0} audiobooks (${seriesCount} with series)${result.failed ? `, ${result.failed} failed` : ""}${result.skipped ? `, ${result.skipped} skipped` : ""}` ); } @@ -114,7 +114,7 @@ router.post("/sync", requireAuthOrToken, apiLimiter, async (req, res) => { * Debug endpoint to see raw series data from Audiobookshelf */ // Debug endpoint for series data -router.get("/debug-series", requireAuthOrToken, async (req, res) => { +router.get("/debug-series", requireAuthOrToken, async (_req, res) => { if (process.env.NODE_ENV === "production") { return res.status(404).json({ error: "Not found" }); } diff --git a/backend/src/services/audiobookCache.ts b/backend/src/services/audiobookCache.ts index 88e36b6..5bcbe2c 100644 --- a/backend/src/services/audiobookCache.ts +++ b/backend/src/services/audiobookCache.ts @@ -74,23 +74,20 @@ export class AudiobookCacheService { for (const book of audiobooks) { try { - await this.syncAudiobook(book); - result.synced++; - // Extract title and author from nested structure for logging - const metadata = book.media?.metadata || book; - const title = - metadata.title || book.title || "Unknown Title"; - const author = - metadata.authorName || - metadata.author || - book.author || - "Unknown Author"; - logger.debug(` Synced: ${title} by ${author}`); + const synced = await this.syncAudiobook(book); + if (synced) { + result.synced++; + const metadata = book.media?.metadata || book; + const title = metadata.title || book.title || "Unknown Title"; + const author = metadata.authorName || metadata.author || book.author || "Unknown Author"; + logger.debug(` Synced: ${title} by ${author}`); + } else { + result.skipped++; + } } catch (error: any) { result.failed++; const metadata = book.media?.metadata || book; - const title = - metadata.title || book.title || "Unknown Title"; + const title = metadata.title || book.title || "Unknown Title"; const errorMsg = `Failed to sync ${title}: ${error.message}`; result.errors.push(errorMsg); logger.error(` ${errorMsg}`); @@ -117,13 +114,13 @@ export class AudiobookCacheService { /** * Sync a single audiobook */ - private async syncAudiobook(book: any): Promise { + private async syncAudiobook(book: any): Promise { const metadata = book.media?.metadata || book; const title = metadata.title || book.title; if (!title) { logger.warn(` Skipping audiobook ${book.id} - missing title`); - return; + return false; } const author = metadata.authorName || metadata.author || null; @@ -141,7 +138,7 @@ export class AudiobookCacheService { const duration = book.media?.duration || null; const numTracks = book.media?.numTracks || null; const numChapters = book.media?.numChapters || null; - const size = book.size ? BigInt(book.size) : null; + const size = book.media?.size ? BigInt(book.media.size) : null; const libraryId = book.libraryId || null; const coverPath = book.media?.coverPath || null; @@ -265,6 +262,8 @@ export class AudiobookCacheService { lastSyncedAt: new Date(), }, }); + + return true; } /** @@ -322,6 +321,7 @@ export class AudiobookCacheService { headers: { Authorization: `Bearer ${settings.audiobookshelfApiKey}`, }, + signal: AbortSignal.timeout(10_000), }); if (!response.ok) { @@ -434,5 +434,5 @@ export class AudiobookCacheService { } } -// Export singleton instance +// Singleton instance export const audiobookCacheService = new AudiobookCacheService(); diff --git a/docker-compose.yml b/docker-compose.yml index 3bf730f..b06d393 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -77,6 +77,9 @@ services: NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-} # Build type: "nightly" for dev builds, unset/empty for tagged releases NEXT_PUBLIC_BUILD_TYPE: ${NEXT_PUBLIC_BUILD_TYPE:-nightly} + # Backend URL for Next.js rewrites (compiled at build time). + # In multi-container mode, frontend proxies to the backend service. + NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL:-http://backend:3006} container_name: kima_frontend environment: NODE_ENV: ${NODE_ENV:-production} diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 847c4a2..1155e8a 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -22,11 +22,13 @@ COPY --from=deps /app/package*.json ./ # Copy source code COPY . . -# Accept build args for environment variables +# Build args for environment variables ARG NEXT_PUBLIC_API_URL ARG NEXT_PUBLIC_BUILD_TYPE=nightly +ARG NEXT_PUBLIC_BACKEND_URL ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL ENV NEXT_PUBLIC_BUILD_TYPE=$NEXT_PUBLIC_BUILD_TYPE +ENV NEXT_PUBLIC_BACKEND_URL=$NEXT_PUBLIC_BACKEND_URL # Build Next.js application RUN npm run build diff --git a/frontend/app/audiobooks/page.tsx b/frontend/app/audiobooks/page.tsx index 86353ce..cba9ab8 100644 --- a/frontend/app/audiobooks/page.tsx +++ b/frontend/app/audiobooks/page.tsx @@ -9,7 +9,8 @@ import { api } from "@/lib/api"; import { useAudioState, useAudioControls } from "@/lib/audio-context"; import { useAuth } from "@/lib/auth-context"; import { useToast } from "@/lib/toast-context"; -import { useAudiobooksQuery } from "@/hooks/useQueries"; +import { useAudiobooksQuery, queryKeys } from "@/hooks/useQueries"; +import { useQueryClient } from "@tanstack/react-query"; import { Book, ListTree, @@ -17,6 +18,7 @@ import { ChevronLeft, ChevronRight, ArrowUpDown, + RefreshCw, } from "lucide-react"; import { shuffleArray } from "@/utils/shuffle"; @@ -66,7 +68,9 @@ export default function AudiobooksPage() { const { currentAudiobook } = useAudioState(); const { pause } = useAudioControls(); + const queryClient = useQueryClient(); const { data: audiobooksData, isLoading, error } = useAudiobooksQuery(); + const [isSyncing, setIsSyncing] = useState(false); const [filter, setFilter] = useState("all"); const [sortBy, setSortBy] = useState("title"); @@ -233,6 +237,24 @@ export default function AudiobooksPage() { } }; + const handleSync = async () => { + if (isSyncing) return; + setIsSyncing(true); + try { + const res = await api.post<{ result: { synced: number; failed: number; skipped: number } }>("/audiobooks/sync", {}); + const result = res?.result; + const parts = [`Synced ${result?.synced ?? 0} audiobooks`]; + 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() }); + } catch { + toast.error("Audiobook sync failed"); + } finally { + setIsSyncing(false); + } + }; + if (isLoading) { return (
@@ -376,6 +398,16 @@ export default function AudiobooksPage() { Random + + {filteredBooks.length} {filteredBooks.length === 1 ? "book" : "books"} diff --git a/frontend/package.json b/frontend/package.json index 744773f..0f709ea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "kima-frontend", - "version": "1.7.1", + "version": "1.7.2", "description": "Kima web frontend", "license": "GPL-3.0", "repository": {