diff --git a/apps/backoffice/app/libraries/[id]/books/page.tsx b/apps/backoffice/app/libraries/[id]/books/page.tsx
new file mode 100644
index 0000000..6181a60
--- /dev/null
+++ b/apps/backoffice/app/libraries/[id]/books/page.tsx
@@ -0,0 +1,95 @@
+import { fetchLibraries, fetchBooks, getBookCoverUrl, LibraryDto, BookDto } from "../../../../lib/api";
+import { BooksGrid, EmptyState } from "../../../components/BookCard";
+import { Card, Badge, Button, CursorPagination } from "../../../components/ui";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+
+export const dynamic = "force-dynamic";
+
+export default async function LibraryBooksPage({
+ params,
+ searchParams
+}: {
+ params: Promise<{ id: string }>;
+ searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
+}) {
+ const { id } = await params;
+ const searchParamsAwaited = await searchParams;
+ const cursor = typeof searchParamsAwaited.cursor === "string" ? searchParamsAwaited.cursor : undefined;
+ const series = typeof searchParamsAwaited.series === "string" ? searchParamsAwaited.series : undefined;
+ const limit = typeof searchParamsAwaited.limit === "string" ? parseInt(searchParamsAwaited.limit) : 20;
+
+ const [library, booksPage] = await Promise.all([
+ fetchLibraries().then(libs => libs.find(l => l.id === id)),
+ fetchBooks(id, series, cursor, limit).catch(() => ({
+ items: [] as BookDto[],
+ next_cursor: null
+ }))
+ ]);
+
+ if (!library) {
+ notFound();
+ }
+
+ const books = booksPage.items.map(book => ({
+ ...book,
+ coverUrl: getBookCoverUrl(book.id)
+ }));
+ const nextCursor = booksPage.next_cursor;
+
+ const seriesDisplayName = series === "unclassified" ? "Unclassified" : series;
+ const hasNextPage = !!nextCursor;
+ const hasPrevPage = !!cursor;
+
+ return (
+ <>
+
+ ← Back to libraries
+
+
+
+
+ {library.name}
+
+
+
+
+ {library.root_path}
+ |
+ {library.book_count} book{library.book_count !== 1 ? 's' : ''}
+ |
+
+ {library.enabled ? "Enabled" : "Disabled"}
+
+
+
+
+
+
+ {series ? `Books in "${seriesDisplayName}"` : "All Books"}
+
+ {series && (
+
+ View all
+
+ )}
+
+
+ {books.length > 0 ? (
+ <>
+
+
+
+ >
+ ) : (
+
+ )}
+ >
+ );
+}
diff --git a/apps/backoffice/app/libraries/[id]/series/page.tsx b/apps/backoffice/app/libraries/[id]/series/page.tsx
new file mode 100644
index 0000000..d3a178d
--- /dev/null
+++ b/apps/backoffice/app/libraries/[id]/series/page.tsx
@@ -0,0 +1,87 @@
+import { fetchLibraries, fetchSeries, getBookCoverUrl, LibraryDto, SeriesDto } from "../../../../lib/api";
+import Image from "next/image";
+import Link from "next/link";
+import { notFound } from "next/navigation";
+import { Card, Badge } from "../../../components/ui";
+
+export const dynamic = "force-dynamic";
+
+export default async function LibrarySeriesPage({
+ params
+}: {
+ params: Promise<{ id: string }>;
+}) {
+ const { id } = await params;
+
+ const [library, series] = await Promise.all([
+ fetchLibraries().then(libs => libs.find(l => l.id === id)),
+ fetchSeries(id).catch(() => [] as SeriesDto[])
+ ]);
+
+ if (!library) {
+ notFound();
+ }
+
+ return (
+ <>
+
+ ← Back to libraries
+
+
+
+
+ {library.name}
+
+
+
+
+ {library.root_path}
+ |
+ {library.book_count} book{library.book_count !== 1 ? 's' : ''}
+ |
+
+ {library.enabled ? "Enabled" : "Disabled"}
+
+
+
+
+ Series ({series.length})
+
+ {series.length > 0 ? (
+
+ {series.map((s) => (
+
+
+
+
+
+
+
+ {s.name === "unclassified" ? "Unclassified" : s.name}
+
+
+ {s.book_count} book{s.book_count !== 1 ? 's' : ''}
+
+
+
+
+ ))}
+
+ ) : (
+
+
No series found in this library
+
+ )}
+ >
+ );
+}
diff --git a/apps/backoffice/app/libraries/page.tsx b/apps/backoffice/app/libraries/page.tsx
new file mode 100644
index 0000000..d9f65d0
--- /dev/null
+++ b/apps/backoffice/app/libraries/page.tsx
@@ -0,0 +1,181 @@
+import { revalidatePath } from "next/cache";
+import Link from "next/link";
+import { listFolders, createLibrary, deleteLibrary, fetchLibraries, fetchSeries, scanLibrary, LibraryDto, FolderItem } from "../../lib/api";
+import { LibraryActions } from "../components/LibraryActions";
+import { Card, CardHeader, Button, Badge, FormField, FormInput, FormSelect, FormRow } from "../components/ui";
+
+export const dynamic = "force-dynamic";
+
+function formatNextScan(nextScanAt: string | null): string {
+ if (!nextScanAt) return "-";
+ const date = new Date(nextScanAt);
+ const now = new Date();
+ const diff = date.getTime() - now.getTime();
+
+ if (diff < 0) return "Due now";
+ if (diff < 60000) return "< 1 min";
+ if (diff < 3600000) return `${Math.floor(diff / 60000)}m`;
+ if (diff < 86400000) return `${Math.floor(diff / 3600000)}h`;
+ return `${Math.floor(diff / 86400000)}d`;
+}
+
+export default async function LibrariesPage() {
+ const [libraries, folders] = await Promise.all([
+ fetchLibraries().catch(() => [] as LibraryDto[]),
+ listFolders().catch(() => [] as FolderItem[])
+ ]);
+
+ const seriesCounts = await Promise.all(
+ libraries.map(async (lib) => {
+ try {
+ const series = await fetchSeries(lib.id);
+ return { id: lib.id, count: series.length };
+ } catch {
+ return { id: lib.id, count: 0 };
+ }
+ })
+ );
+
+ const seriesCountMap = new Map(seriesCounts.map(s => [s.id, s.count]));
+
+ async function addLibrary(formData: FormData) {
+ "use server";
+ const name = formData.get("name") as string;
+ const rootPath = formData.get("root_path") as string;
+ if (name && rootPath) {
+ await createLibrary(name, rootPath);
+ revalidatePath("/libraries");
+ }
+ }
+
+ async function removeLibrary(formData: FormData) {
+ "use server";
+ const id = formData.get("id") as string;
+ await deleteLibrary(id);
+ revalidatePath("/libraries");
+ }
+
+ async function scanLibraryAction(formData: FormData) {
+ "use server";
+ const id = formData.get("id") as string;
+ await scanLibrary(id);
+ revalidatePath("/libraries");
+ revalidatePath("/jobs");
+ }
+
+ async function scanLibraryFullAction(formData: FormData) {
+ "use server";
+ const id = formData.get("id") as string;
+ await scanLibrary(id, true);
+ revalidatePath("/libraries");
+ revalidatePath("/jobs");
+ }
+
+ return (
+ <>
+
+
+ Libraries
+
+
+ {/* Add Library Form */}
+
+
+
+
+
+ {/* Libraries Grid */}
+
+ {libraries.map((lib) => {
+ const seriesCount = seriesCountMap.get(lib.id) || 0;
+ return (
+
+ {/* Header with settings */}
+
+
+
{lib.name}
+ {!lib.enabled && Disabled}
+
+
+
+
+ {/* Path */}
+ {lib.root_path}
+
+ {/* Stats */}
+
+
+ {lib.book_count}
+ Books
+
+
+ {seriesCount}
+ Series
+
+
+
+ {/* Status */}
+
+
+ {lib.monitor_enabled ? '●' : '○'} {lib.monitor_enabled ? 'Auto' : 'Manual'}
+
+ {lib.watcher_enabled && (
+ ⚡
+ )}
+ {lib.monitor_enabled && lib.next_scan_at && (
+
+ Next: {formatNextScan(lib.next_scan_at)}
+
+ )}
+
+
+ {/* Actions */}
+
+
+
+
+
+
+ );
+ })}
+
+ >
+ );
+}