refactor: config-driven libraries; drop folder-add UI

The frontend AddSourceFolderDialog let users register source roots
from inside the app, but with the bootstrap auto-creating one for
the /photos mount on first boot, the dialog was redundant in the
common case and confusing in every other (users had to know which
container path corresponded to their host directory). Going
config-driven matches Plex/Photoprism/Immich and matches the
mental model "the docker mount IS the library".

Frontend
- Deleted components/dialogs/AddSourceFolderDialog.tsx entirely.
- LeftSidebar drops the "+ Add Source Folder" button + bottom-bar
  layout, the addFolderMutation, the dead Plus action button on
  the (no-longer-existing) folders/heaps tree headers, and the
  Plus icon import.
- api.ts: removed sourceFolders.add(), library.browse(), and the
  BrowseChild / BrowseResponse types. The remaining sourceFolders
  surface is read-only (list + manual scan).
- LeftSidebar bottom strip is now just the "Scan all folders"
  button when there's at least one source root.

Backend
- Dropped POST /folders (no consumers) along with FolderCreate /
  FolderResponse pydantic models. The folders router header now
  documents the config-driven approach.
- Dropped GET /library/browse (no consumers). Removed the unused
  os/HTTPException/SourceRoot imports it brought in.
- cleanup_data_integrity now also walks the source roots and logs
  a warning for any whose path is missing on disk. Doesn't auto-
  delete (a missing path could be a temporarily unmounted drive)
  but surfaces enough hint to fix it. Returns the count in the
  summary dict alongside merged-duplicates.

Docs
- README "How libraries are managed" section rewritten to spell
  out that mounts ARE source roots, edit .env + restart, no UI for
  managing source roots. New "Changing or adding libraries"
  section walks through the typical edit-restart loop including
  the optional volume-nuke for a clean slate.
- "Adding more libraries" subsection covers multi-mount via
  edited compose with a note that auto-registration is roadmap.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 00:44:48 +02:00
parent 204d2bf2a8
commit 320107841b
8 changed files with 65 additions and 482 deletions

View File

@@ -6,13 +6,11 @@ import {
Image,
Star,
Trash2,
Plus,
MoreHorizontal,
HardDrive,
RefreshCw,
} from 'lucide-react'
import clsx from 'clsx'
import { AddSourceFolderDialog } from '../dialogs/AddSourceFolderDialog'
import { sourceFolders, library, photos as photosApi } from '../../services/api'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from '../ToastContainer'
@@ -32,7 +30,6 @@ interface TreeItem {
export function LeftSidebar() {
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set(['library', 'folders', 'heaps']))
const [selectedItem, setSelectedItem] = useState<string | null>('all-photos')
const [showAddFolderDialog, setShowAddFolderDialog] = useState(false)
const [isScanning, setIsScanning] = useState(false)
const queryClient = useQueryClient()
@@ -119,32 +116,11 @@ export function LeftSidebar() {
}
// Fetch folders from API
const { data: foldersData, refetch: refetchFolders } = useQuery({
const { data: foldersData } = useQuery({
queryKey: ['folders'],
queryFn: sourceFolders.list,
})
// Mutation for adding folders
const addFolderMutation = useMutation({
mutationFn: async ({ path, recursive }: { path: string; recursive: boolean }) => {
// Add the folder
const folder = await sourceFolders.add(path, recursive)
// Trigger scan for the new folder
await sourceFolders.scan(folder.id)
return folder
},
onSuccess: (folder) => {
toast.success('Folder Added', `Scanning ${folder.name || folder.path}...`)
// Refetch folders list
refetchFolders()
// Refetch photos to show new ones
queryClient.invalidateQueries({ queryKey: ['photos'] })
},
onError: (error: any) => {
toast.error('Failed to Add Folder', error.message || 'An error occurred')
},
})
// Mutation for scanning all folders
const scanLibraryMutation = useMutation({
mutationFn: library.scan,
@@ -165,10 +141,6 @@ export function LeftSidebar() {
},
})
const handleAddFolder = async (path: string, recursive: boolean) => {
await addFolderMutation.mutateAsync({ path, recursive })
}
const handleScanAll = () => {
scanLibraryMutation.mutate()
}
@@ -320,18 +292,6 @@ export function LeftSidebar() {
</span>
)}
{/* Actions (shown on hover) */}
{(item.id === 'folders' || item.id === 'heaps') && (
<button
onClick={(e) => {
e.stopPropagation()
// Handle add folder/heap
}}
className="invisible rounded p-0.5 text-text-muted hover:bg-surface-offset hover:text-text group-hover:visible"
>
<Plus className="h-3 w-3" />
</button>
)}
</div>
{/* Render Children */}
@@ -361,32 +321,18 @@ export function LeftSidebar() {
</div>
{/* Bottom Actions */}
<div className="border-t border-border p-3 space-y-2">
<button
onClick={() => setShowAddFolderDialog(true)}
className="flex w-full items-center gap-2 rounded bg-surface-2 px-3 py-2 text-sm text-text hover:bg-surface-offset"
>
<Plus className="h-4 w-4" />
Add Source Folder
</button>
{foldersData?.folders?.length > 0 && (
<button
{foldersData?.folders?.length > 0 && (
<div className="border-t border-border p-3">
<button
onClick={handleScanAll}
disabled={isScanning}
className="flex w-full items-center gap-2 rounded bg-surface-2 px-3 py-2 text-sm text-text hover:bg-surface-offset disabled:opacity-50"
>
<RefreshCw className={clsx("h-4 w-4", isScanning && "animate-spin")} />
{isScanning ? 'Scanning...' : 'Scan All Folders'}
<RefreshCw className={clsx('h-4 w-4', isScanning && 'animate-spin')} />
{isScanning ? 'Scanning...' : 'Scan all folders'}
</button>
)}
</div>
{/* Add Source Folder Dialog */}
<AddSourceFolderDialog
isOpen={showAddFolderDialog}
onClose={() => setShowAddFolderDialog(false)}
onAdd={handleAddFolder}
/>
</div>
)}
</div>
)
}