UI Pass 2 and sync/process improvements#11
Merged
Conversation
…rows The SSE handler in base.html inserted dynamically-appended rows with ASIN in the first column and Title in the second, but the completed and failed table headers (in downloads.html and dashboard_downloads.html) order columns as Title, ASIN. Rows added on download completion appeared with swapped values until a full page refresh re-rendered them server-side. Reorder the inserted cells to match the header order, and run the values through escHTML to match the rest of the SSE-driven UI.
The library search input previously matched only the title and author columns, so a query like "Dungeon Crawler" returned only the books where the series name happened to also appear in the title. Extend the SQLite and PostgreSQL WHERE-clause builders to also LIKE match the series and asin columns. Series is stored as a plain string column on books so no schema changes are needed.
The dashboard and destinations page computed each destination's coverage % as item_count / books_with_status_complete, capped at 100. When a destination held more items than the DB tracked as complete — common when destination items linger across status transitions or when reconciliation hasn't refreshed the book records — the cap silently turned every card into "100%", hiding real coverage gaps (e.g. ABS at 636 items, Plex at 629, against ~650 in the library). Switch the denominator to the total book count so the metric reads as "fraction of the library present in this destination". The totalBooks count is far more stable than the complete count across sync transitions, so the cap rarely kicks in for legitimate state.
Books downloaded under the historical Basic tag profile (or restored from older runs) ship without an ASIN atom embedded in the m4b, which makes Audiobookshelf's library-matcher unable to identify them and forces a manual re-download to fix. Introduce a new Metadata Repair phase that runs as part of the full sync pipeline (and can be triggered standalone via the existing /api/sync/phase/:phase endpoint): - ffprobes every complete book's file and compares the embedded ASIN atom to the DB record - when missing or stale, re-runs EmbedMetadata with the AudiobookRich profile regardless of the user's tag_profile setting, since the whole point of this job is to make ASIN matching work - enriches via Audnexus when available so genre/copyright/etc. match what the original download would have written; falls back to the DB-only field set when audnexus is unreachable - writes the new file to a sibling temp path and atomically renames over the original, so a crashed ffmpeg never corrupts the source file ffmpeg uses -c copy so the actual operation is a fast remux with no re-encode. Phase is skipped (not failed) when ffmpeg isn't available on the host.
The library page had no way to ask "which of my books aren't on Plex/ABS yet?" — presence was visible only as per-row chips, with no way to slice the table by it. Status tabs were the only filter dimension, and presence didn't fit there anyway since a book can be Complete and missing from a destination at the same time. Add three orthogonal presence dimensions to BookFilter: OnDisk *bool — file_path != '' PresentInDestinations []id — EXISTS row with sync_state in synced|syncing MissingFromDestinations []id — NOT EXISTS the above Wire them through: - BookFilter struct and both WHERE-clause builders (sqlite, postgres), using EXISTS subqueries that hit the (destination_id, sync_state) index on book_library_destinations. - handleLibrary URL params: ?on_disk=yes|no plus a dynamic ?presence_<dest_id>=in|out for each enabled destination. - Filter bar dropdowns: one for disk state, one per enabled destination, generated from the live destinations list so the UI tracks whatever the user has configured. - Status tabs stay status-only — presence is now a filter, not a tab, so it composes with status (e.g. "Complete AND missing from ABS") instead of being mutually exclusive. Also drop the surface-2 background from .presence-chip so the row chips inherit from the table cell instead of painting their own fill.
Two additions to the book detail panel: 1. File info block (only when the book is on disk) New ProbeFile method on the FFmpeg wrapper that runs ffprobe with -show_format -show_streams -show_chapters in a single invocation and returns a FileProbe carrying container/codec technicals (format, codec, bitrate, sample rate, channels, channel layout, duration), chapter count, embedded-artwork flag, and the raw format-level tag map. handleBookDetail composes a bookFileInfoView from that probe plus os.Stat (size, modified time), the filesystem path, and the account's activation bytes (fetched from the audible client when authenticated — surfaced here because they're what was used to decrypt this file, useful for re-decryption diagnostics). The template renders it as a meta-grid mirroring the existing metadata stack, with the raw container tags tucked inside a <details> summary so encoder/major_brand/compatible_brands/asin etc. stay one click away. Container-identity tags bubble to the top of that list; everything else sorts alphabetically. Every field is independently optional — a degraded probe still renders size + modified date + path so the block degrades usefully instead of disappearing. 2. Collapsible description Long Audible descriptions clamp to 9.5rem with a fade-out gradient over the bottom 3rem; a "Show more" button toggles to "Show less". The script hides the toggle when the clamped content already fits (scrollHeight <= clientHeight) so short descriptions don't get a dangling affordance. Wires ffmpeg through web.NewServer so handlers can call ProbeFile directly — previously ffmpeg lived only inside DownloadManager and SyncService.
Three issues touching the dashboard, sidebar, and library table that all show up the moment a user reaches for a phone or does a sync. 1. Dashboard didn't update after sync. The summary panel listened for an undefined "queue:last" event, so its only refresh path was the 12s poll — meaning the "Library Items" / "New" counters stayed stale for up to 12s after the user kicked a Quick Sync that surfaced new books. Add "sync:done from:body" to the dashboard summary and downloads hx-trigger lists, and have the SSE sync event handler in base.html fire that event whenever sync transitions out of the running state. The dashboard refetches immediately when sync ends instead of waiting for the next poll tick. 2. The "New" stat card on the dashboard was a non-interactive div even though "go look at my N new books" is the obvious next action. Wrap it in an anchor to /library?status=new with no text-decoration; subtle hover ring keeps the affordance visible without trumpeting it. 3. Mobile sidebar wasn't usable. The existing 768px breakpoint flipped the sidebar into a horizontal bar that wrapped six nav links and consumed half the screen. Replace with an off-canvas drawer: hamburger button in the topbar, body.sidebar-open slides the drawer in via transform, a backdrop closes on click, nav links auto-close on activation, Escape dismisses. 4. Library table wasn't usable on mobile either. Nine columns can't reflow on a phone-width viewport without horizontal scroll. At ≤768px, the table flips to a card layout: cover floats left, title/author/series stack as block lines, and the dense bits (duration, purchased, status, presence chips) pack into a single wrapping inline-flex row. Actions clear the float into a full-width footer with a top border. Named column classes (col-title, col-author, …) added to the row template so future column reorders don't break the layout.
The left-rail "On this page" highlight used an IntersectionObserver that only saw the sections whose visibility changed in each batch, so clicking a link (or scrolling) could snap the highlight back to the top section. Replaced it with a position-based scroll-spy that evaluates every section on each scroll, anchors to the last section at the bottom of the page, and locks itself while a click-driven smooth scroll animates so it can't fight the click. Also fixed the post-auth focus helper, which looked for a non-existent "<name>-settings" element id instead of "section-<name>". Files: internal/web/templates/settings.html Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New installs now default to the Audiobook-rich tag profile, which embeds series, series-part, and asin atoms so apps like Audiobookshelf can group series automatically. The profile is additive (it keeps the album=title workaround that stops Plex collapsing a series), so the switch is safe for existing libraries too. Added a DefaultTagProfile constant and routed all fallbacks (unset, nil DB, unknown value) through it. Files: internal/audio/tag_profile.go, internal/audio/tag_profile_test.go Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Added a toggle on the final setup step (on by default) that controls whether books found during a sync are queued for download right away. The choice is wired to the existing "Start sync" form and saved when setup finishes, so new users get to opt in or out instead of inheriting a silent default. Files: internal/web/templates/setup.html, internal/web/setup_handlers.go Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sync schedule, enable-sync toggle, sync mode, and auto-download setting already had live setters on the scheduler, but the settings handler only wrote them to the database and asked for a restart because the web server never received a scheduler reference. Wired the scheduler into the web server so saving those settings reconfigures the running scheduler immediately. Added a mutex to the scheduler since its fields are now touched by both the cron goroutine and the web handler. Worker counts are the only setting that still needs a restart (the pipeline goroutines are sized once at startup), and the save message now says so specifically. Files: internal/scheduler/cron.go, internal/web/server.go, cmd/server/main.go Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reworded settings copy to read more simply: renamed "Auto-add new books to queue" to "Automatically download new books", simplified the worker hints and sync mode labels, refreshed the tag profile help to lead with the new default, and updated the page subtitle. Also removed the now-incorrect "applies after a restart" note from the Sync section (those settings apply live) and tightened the surviving worker-count restart note. Files: internal/web/templates/settings.html Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Every "At a glance" card now links to its matching library filter: Library Items to the full library, Downloaded to complete, In Progress to downloading, and Failed to failed (New already linked). They reuse the existing stat-card-link style, so there is no underline and the same hover and focus treatment applies. Files: internal/web/templates/dashboard_summary.html Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On every library view, books that aren't downloaded now show Download (or Retry, for failed books) as the primary button, with View moved into the More menu. Downloaded and unavailable books keep View as the primary button since there is nothing to download. The cover and title stay clickable, so book details are always one click away. Files: internal/web/templates/library_row.html Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Diagnostics page links to books by ASIN, but the book detail handler only parsed numeric IDs, so it fell through to a bare error render that showed just the page shell. The handler now accepts either a numeric book ID or an ASIN (numeric IDs and ASINs never overlap), so those links open the correct book. Files: internal/web/server.go Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… step The setup wizard's Storage step only displayed the container's mount paths, which can't be edited from the app, so it was removed. Those paths still appear (read-only) under Settings > System Information, which now explains that they come from the container's volume mounts. Also fixed uneven gaps between the Library/Sync/Performance cards, which were touching because they sit inside one form that the settings grid's gap couldn't reach. Files: internal/web/templates/setup.html, internal/web/setup_handlers.go, internal/web/templates/settings.html, internal/web/server.go Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR delivers a second UI polish pass across the web app (setup wizard, settings, dashboard, library, and book detail) and pairs it with several sync/scheduling and data-correctness enhancements (metadata repair phase, richer search/presence filtering, and improved destination coverage math).
Changes:
- Improves onboarding + navigation UX (wizard step cleanup, dashboard cards clickable, mobile nav drawer, settings scroll behavior).
- Expands library/book-detail UX (presence filters, richer search, row action tweaks, file-info panel, collapsible descriptions, mobile-friendly library rows).
- Adds/adjusts sync + backend behavior (metadata repair sync phase, live-apply sync scheduler settings, destination coverage denominator update, new DB filter support + tests, tag profile default update).
Reviewed changes
Copilot reviewed 23 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/web/templates/setup.html | Removes storage wizard step and adds auto-download-new-books toggle on final step. |
| internal/web/templates/settings.html | Updates settings copy, section focusing, and replaces TOC scroll-spy implementation. |
| internal/web/templates/library.html | Adds disk/presence filters and consolidates HTMX includes via .lib-filter. |
| internal/web/templates/library_row.html | Refines table column classes for responsive layout; makes Download/Retry primary for undownloaded rows. |
| internal/web/templates/dashboard.html | Refreshes summary/download panels on sync:done events in addition to polling. |
| internal/web/templates/dashboard_summary.html | Makes “At a glance” stat cards clickable links into library filters. |
| internal/web/templates/book_detail_panel.html | Adds “File info” block and collapsible description behavior. |
| internal/web/templates/base.html | Adds mobile nav drawer + backdrop; triggers sync:done and hardens dynamic row HTML escaping. |
| internal/web/static/style.css | Styles clickable stat cards, mobile nav drawer/backdrop, responsive library “card view”, and book detail additions. |
| internal/web/setup_handlers.go | Updates setup step list and persists final-step auto-queue setting. |
| internal/web/server.go | Threads scheduler/ffmpeg into web server; updates destination coverage math; adds library presence filters; adds ASIN support for book detail; applies sync settings live via scheduler; adds file-probe view model. |
| internal/web/destinations_handlers.go | Updates destination coverage denominator to use total library size. |
| internal/scheduler/cron.go | Adds mutex protection for live scheduler reconfiguration and cron entry updates. |
| internal/library/sync.go | Introduces Metadata Repair sync phase and optional dependency wiring for ffmpeg/audnexus. |
| internal/library/metadata_repair.go | Implements metadata repair pass that probes tags and re-embeds AudiobookRich atoms when needed. |
| internal/database/sqlite.go | Extends search fields and adds on-disk + per-destination presence filters in WHERE builder. |
| internal/database/postgres.go | Mirrors sqlite search/presence filter behavior for Postgres. |
| internal/database/interface.go | Extends BookFilter with on-disk and destination presence predicates. |
| internal/database/books_presence_filter_test.go | Adds coverage tests for the new presence filters (SQLite). |
| internal/audio/tag_profile.go | Changes default tag profile behavior to prefer AudiobookRich for new/unknown settings. |
| internal/audio/tag_profile_test.go | Updates tests for the new default tag profile behavior. |
| internal/audio/probe_tags.go | Adds ffprobe helpers for reading tags and full file probe details for UI. |
| cmd/server/main.go | Wires optional sync dependencies and updates web server constructor args. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Theme: UI Pass 2 — settings cleanup, dashboard clickability, richer book detail, smarter library filters/search, and a handful of sync/data correctness fixes.
Settings & Wizard
Dashboard & Navigation
Library & Book Detail
Sync & Data