Frontend half of the faceted-navigation Phase 2. 'Everything filterable
should be clickable': every displayed facet value links to the bookshelf
pre-filtered by its raw stored value; display forms are prettified at
render, never in the DB.
- templates/facet_links.templ: FacetLink (inline links) + FacetBadge
(badge links) to /bookshelf?<facet>_filter=<value>; the raw value
rides the param while the shown text is prettified, so matching and
display can never disagree. Series/tags keep their dedicated pages
by design; the card media-type icon stays unlinked (a derived kind,
not a column value — deep-linking it would send wrong values).
- book_detail.templ: publisher, language, edition, genre, imprint
cells + narrators line + age-rating/story-arc badges are now links.
- prettifyGenre (templates/utils.go + unit tests): title-cases only
entirely-lowercase values; small words stay down except first
position; & preserved. TS port for chip labels documents the twin.
- bookshelf.templ + bookshelf.ts: active-filter chips bar — Alpine
renders chips from client-local form state (sanctioned island);
one bubbled htmx:afterRequest hook keeps chips + shareable URL in
sync; removeFilterChip clears the facet and re-applies. URL deep
links now accept all 18 facet params (hidden inputs where the
drawer has no field).
Pins changelog: Batch B/C entry in docs/contract_pins.md. Rendering
ownership guard (make lint-rendering) green.
The audiobook library type shipped backend-complete (LibraryTypeAudiobooks
constant, allowed extensions, MIME map, schema seed row, GET
/api/libraries/types) but neither create-library form offered it: the
admin Create Library modal and the setup wizard's add-library step both
hardcoded their <option> lists to the original three types
(ebooks/comics/manga), so an audiobook library could not be created from
the UI.
Both dropdowns now render from the same DB-backed library_types table
the create API resolves types against — a future type appears without
template edits (the exact drift the rendering-ownership rule warns
about):
- templates/utils.go: OrderedLibraryTypes pins the historical dropdown
order (ebooks, comics, manga, audiobooks) and appends unknown future
types by name; LibraryTypeDisplayName renders labels with a
capitalize-the-name fallback for types added without a label
- templates/admin_library.templ: new LibraryTypeOptions fragment used
by the modal's select; AdminLibrary takes the types list
- templates/setup.templ: Setup takes the types list, same fragment
- internal/router/helpers.go: libraryTypeOptionsFor fetches via
LibraryService.GetLibraryTypes, falling back to the built-in type
names on DB failure so the forms stay usable (CreateLibrary still
resolves the name against the DB, so the fallback is never
authoritative)
- internal/router/frontend.go, internal/router/setup.go: pass the list
into the two pages
Tests:
- templates/utils_test.go: pure unit tests for ordering + labels
- cmd/server/tests/admin_library_page_test.go: SSR regression —
/admin/library must offer all four library types, audiobooks exactly
once (options come from the DB loop, not a hardcode)
The checkmark in the Appearance theme menu was rendered server-side
(if user.Theme == opt.Name), so it never moved after switching themes
in the browser.
- Always render the check for every theme option, hidden by default,
using a new themeCheckClass(name, current) helper that returns the
hidden class unless the option is the active theme.
- Give each theme button a data-theme attribute and add
updateThemeIndicators() to web/src/theme.ts, which reads the applied
theme from the body class (theme-<name>) and toggles the hidden class
on each check accordingly.
- Call updateThemeIndicators() from changeTheme (before the async save
and on failure), initializeTheme, and loadUserTheme so the menu stays
in sync with the applied theme.
- Add unit test for themeCheckClass (templates/utils_test.go) and
regenerate templ output.
Replace the read-only "System Information" card (which listed hardcoded
values) with editable HTMX forms, organized so the live vs restart
distinction and related settings are visually clear.
admin_settings.templ:
- AdminSettings signature now takes liveGroups and restartGroups
([]SettingGroup) instead of a flat entry list.
- Remove the static System Information list. Render two cards: "Live"
(green, applies immediately) and "Restart Required" (warning header,
saved but only takes effect after restart).
- Within each card, TunableSettingsSection clusters entries into
labeled sub-sections by Group (e.g. "Password Quality", "Device Rate
Limits", "Login Lockout", "Worker Pool") with uppercase tracked
sub-headers.
- TunableSettingRow renders an inline HTMX form per setting: a Yes/No
select for bools, a number input with min/max for ints, text
otherwise, posting to /admin/settings/tunable. Rows show "modified
from default" when the value differs from the compiled default.
types.go:
- Add SettingEntry (template-local mirror of database.SettingEntry,
keeps templates from importing database) and SettingGroup.
utils.go:
- Add GroupTunableSettings: splits a flat, group-sorted entry list into
live and restart []SettingGroup buckets preserving source order.
utils_test.go covers the multi-group + empty cases.
frontend.go:
- The /admin/settings page handler now loads entries from the registry,
drops the three keys that have dedicated UI cards (default_timezone
dropdown, scan_poll_interval_seconds, auto_scan_enabled) so they are
not listed twice, groups the rest, and passes liveGroups/restartGroups
into the template.