diff --git a/cmd/server/tests/admin_library_page_test.go b/cmd/server/tests/admin_library_page_test.go
new file mode 100644
index 00000000..2170d8b2
--- /dev/null
+++ b/cmd/server/tests/admin_library_page_test.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+ "net/http"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// The Library Type dropdown on /admin/library (and the setup wizard, which
+// shares LibraryTypeOptions) must render from the DB-backed library_types
+// table. Regression: audiobooks shipped backend-complete (constant, allowed
+// extensions, MIME map, schema seed row, /api/libraries/types) while the
+// dropdowns hardcoded the original three types, so an audiobook library
+// could not be created from the UI.
+func TestAdminLibraryPageDropdownCoversAllLibraryTypes(t *testing.T) {
+ if testing.Short() {
+ t.Skip("Skipping integration test in short mode")
+ }
+ setup := setupTestServer(t)
+
+ code, html := ssrGet(t, setup.Server.URL+"/admin/library", setup.Token)
+ require.Equal(t, http.StatusOK, code)
+
+ for _, typ := range []string{"ebooks", "comics", "manga", "audiobooks"} {
+ assert.Contains(t, html, `value="`+typ+`"`,
+ "library type %q missing from the create-library dropdown", typ)
+ }
+ assert.Contains(t, html, "Audiobooks")
+
+ // Options come from the DB loop, not the deleted hardcode: exactly one
+ // audiobooks option per page.
+ assert.Equal(t, 1, strings.Count(html, `value="audiobooks"`))
+}
diff --git a/internal/router/frontend.go b/internal/router/frontend.go
index e9d73033..3923ee1b 100644
--- a/internal/router/frontend.go
+++ b/internal/router/frontend.go
@@ -1143,7 +1143,8 @@ func registerFrontendRoutes(cfg *Config) {
var buf bytes.Buffer
archivedCount, _ := cfg.Queries.CountArchivedMediaItems(c.Request().Context())
- err = templates.AdminLibrary(user, libData, userData, archivedCount).Render(c.Request().Context(), &buf)
+ libraryTypes := libraryTypeOptionsFor(c.Request().Context(), cfg)
+ err = templates.AdminLibrary(user, libData, userData, archivedCount, libraryTypes).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
diff --git a/internal/router/helpers.go b/internal/router/helpers.go
index 90a494a9..9d7cfb4a 100644
--- a/internal/router/helpers.go
+++ b/internal/router/helpers.go
@@ -405,3 +405,24 @@ func (cfg *Config) loadCollectionBooksLite(c *echo.Context, userUUID uuid.UUID,
return cfg.collectionBooksForScope(c, userUUID, pgtype.UUID{Bytes: collUUID, Valid: true}, collection, collectionID,
libUUID, libUUID, libraryID != "", visibleIDs)
}
+
+// libraryTypeOptionsFor loads the library_types rows for the create-library
+// dropdowns (admin page + setup wizard), ordered for display. On DB failure
+// it falls back to the built-in type names so the form stays usable —
+// CreateLibrary resolves the name against the DB, so this is never
+// authoritative.
+func libraryTypeOptionsFor(ctx context.Context, cfg *Config) []database.LibraryTypes {
+ types, err := cfg.LibraryService.GetLibraryTypes(ctx)
+ if err != nil {
+ log.Printf("libraryTypeOptionsFor: GetLibraryTypes failed, using built-in type names: %v", err)
+ for _, name := range []string{
+ services.LibraryTypeEbooks,
+ services.LibraryTypeComics,
+ services.LibraryTypeManga,
+ services.LibraryTypeAudiobooks,
+ } {
+ types = append(types, database.LibraryTypes{Name: name})
+ }
+ }
+ return templates.OrderedLibraryTypes(types)
+}
diff --git a/internal/router/setup.go b/internal/router/setup.go
index cd9f71ad..61accfc6 100644
--- a/internal/router/setup.go
+++ b/internal/router/setup.go
@@ -81,7 +81,8 @@ func registerSetupRoutes(cfg *Config) {
return c.Redirect(http.StatusFound, "/")
}
var buf bytes.Buffer
- if err := templates.Setup().Render(c.Request().Context(), &buf); err != nil {
+ libraryTypes := libraryTypeOptionsFor(c.Request().Context(), cfg)
+ if err := templates.Setup(libraryTypes).Render(c.Request().Context(), &buf); err != nil {
log.Printf("Failed to render setup template: %v", err)
return c.HTML(http.StatusInternalServerError, "Failed to render setup page")
}
diff --git a/templates/admin_library.templ b/templates/admin_library.templ
index 63f50839..b522407b 100644
--- a/templates/admin_library.templ
+++ b/templates/admin_library.templ
@@ -1,6 +1,8 @@
package templates
-templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64) {
+import "bookhoard/internal/database"
+
+templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64, libraryTypes []database.LibraryTypes) {
@@ -88,9 +90,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
Library Type
- Ebooks
- Comics
- Manga
+ @LibraryTypeOptions(libraryTypes)
@@ -178,6 +178,15 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
}
+// LibraryTypeOptions renders the
list for a library-type
+// from the DB-backed library_types rows, so new types show up without
+// template edits.
+templ LibraryTypeOptions(types []database.LibraryTypes) {
+ for _, t := range types {
+ { LibraryTypeDisplayName(t.Name) }
+ }
+}
+
templ LibraryList(user User, libraries []LibraryData, users []User) {
if len(libraries) == 0 {
diff --git a/templates/admin_library_templ.go b/templates/admin_library_templ.go
index d6f8892d..a3abd05e 100644
--- a/templates/admin_library_templ.go
+++ b/templates/admin_library_templ.go
@@ -8,7 +8,9 @@ package templates
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
-func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64) templ.Component {
+import "bookhoard/internal/database"
+
+func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64, libraryTypes []database.LibraryTypes) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -87,7 +89,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(archivedCount)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 43, Col: 41}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 45, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -122,7 +124,15 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
Library Name
Description
Library Type ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = LibraryTypeOptions(libraryTypes).Render(ctx, templ_7745c5c3_Buffer)
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "
Cancel ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -130,7 +140,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Create
Edit Library ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Create
Edit Library ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -138,7 +148,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " Library Name
Description
Cancel ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "
Library Name
Description
Cancel ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -146,7 +156,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Save
Browse Folders ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Save
Browse Folders ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -154,7 +164,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "
Delete Library ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
Delete Library ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -162,7 +172,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " Are you sure you want to delete ?
Permanently removed, no archive or undo: the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.Not touched: media files on disk.Cancel ")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "
Are you sure you want to delete ?
Permanently removed, no archive or undo: the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.Not touched: media files on disk.Cancel ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -170,7 +180,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "Delete