UI refresh + first-run onboarding + sync/destination fixes#10
Merged
Conversation
Adds the cross-cutting bits everything else depends on: - Sticky topbar with breadcrumbs and a global search field that submits to /library?search=, plus a "Destinations" link in the sidebar nav. - CSS primitives matching the DesignSystem mockup: .page / .page-header, .section-h, .stat-grid (label/value/delta cards), .tabs with counts, .filter-bar, .table-wrap + .tbl v2 + .table-pagination, .dl-card, .lib-card, .dest-grid + .dest-card with type ribbons + coverage meter, .pool-card refinements, refined .sync-status-v2 + .sync-phases.v2, .modal-overlay shell, and .btn-outline-ds / .btn-ghost / .btn-split. - Global add-destination modal shell in base.html + AudplexusDestModal helper that opens on HTMX swap, listens for HX-Trigger dest-created, closes on backdrop/Escape/×. - Template helpers: firstTwo, joinDestinationNames, coveragePct, add, sub. Registered destination_picker_body and destination_form_body as bare-body template names in multiRender so the same partials can be rendered with or without base layout chrome. Note: server.go and base.html see additional changes in later commits for wizard routes, dashboard JS, destinations modal handlers, library pagination, and diagnostics endpoints. Foundation is committed here so the per-feature commits read cleanly on top.
A 5-step server-rendered wizard (no JS state) that gates new installs: - /setup hosts the wizard; ?step= drives panel visibility. Steps: Welcome, Audible region/sign-in, Storage (read-only paths), add Destinations, Done. - firstRunGate middleware redirects /, /library, /downloads, /destinations to /setup until the "onboarded" setting is "1". Settings, diagnostics, the wizard itself, all POST/api routes pass through so users can always escape and so the wizard's own subroutes keep working. - handleAuthCallback bounces back to /setup?step=2 when in onboarding mode so the Amazon redirect lands in the wizard, not on Settings. - POST /setup/finish marks onboarded=1 and fires a background quick sync if authenticated. /setup/skip and /setup/restart are also wired. The "Re-run Wizard" button in Settings and the factory-reset redirect to /setup land in a later commit alongside the rest of the Settings template refresh. Routes registered in server.go (firstRunGate hookup, /setup/* paths, auth-callback wizard bounce, factory-reset redirect) shipped with the foundation commit.
Restructures the dashboard around the DS layout while keeping the existing HTMX polling + SSE pipeline behaviour intact: - dashboard.html: .page wrapper + .page-header with the existing action cluster (Quick/Full sync, Queue all new, Retry failed, Reorganize, Pause/Resume queue). Two-column "active pipeline + pools" grid below the SSE-driven cards. - dashboard_summary.html: label/value/delta stat cards (with coverage %, waiting count, "all clear"/"needs attention" hints). Destinations rendered with .dest-card + ribbons + coverage meter + type-icon chips. Old text-centred stat-card markup retired in favor of the new grid. - dashboard_downloads.html: section-h headers with counts, .tbl styling for done/failed tables, .empty-card empty states. - sync_status.html: banner head with pulse/ok dot + v2 phase grid via the new .sync-status-v2 / .sync-phases.v2 classes. Phase progress bars and sub-phase rendering kept verbatim. base.html SSE renderers (pool cards + active pipeline cards) need to emit the new class names (.pool-name, .pool-meter, .dl-card v2). Those JS changes shipped with the foundation commit since they live in base.html, which is owned there.
- library.html: .page wrapper + .page-header, status filter tabs with per-status counts (All/New/In Progress/Waiting/Complete/Failed), filter-bar card with search + status + sort + direction selects, kept the existing book-detail modal markup intact. - library_table.html: .table-wrap + .tbl v2, sortable header links that preserve current filter state, new columns (Series, Purchased, On Disk), full pagination control with ellipsis-aware page numbers and "X-Y of N" summary. - library_row.html: cell-title with narrator small-text, mono cells for duration/purchased, on-disk chip (.dest-chip.abs), kept all existing per-row action buttons (Queue/Retry/Convert/Delete/Cancel) and HTMX hookups unchanged. handleLibrary in server.go adds pagination (?page=), libraryStatusCounts for the tab badges, and a paginationNumbers helper for the compact pager. Those shipped with the foundation commit.
Destinations used to live as a section inside Settings. Pulled out into a top-level page with richer per-card detail (item count, coverage, last error, last checked) and a modal-able add flow that the wizard and the destinations page share. - destinations.html (new): page-header with "N of M healthy" subtitle, dest-grid using the v2 dest-card (ribbon + type-icon + coverage meter + inline last-error alert), per-card Test/Edit/Enable/Disable/Delete actions, empty-state CTA opens the modal. - destinations_new.html: extracted "destination_picker_body" partial so it renders both as a full page (/destinations/new) and as a bare body inside the modal. Picker submit target is data-driven (PickerAction). - destinations_form.html: extracted "destination_form_body" partial. Modal mode posts via HTMX with hx-headers X-Dest-Modal=1 so the create handler branches between "redirect to /destinations" and "fire HX-Trigger dest-created + render success fragment". Plex PIN flow, Discover-libraries chains, Test Connection all kept verbatim. - handleDestinationsCreate: branches on X-Dest-Modal. Modal path emits HX-Trigger so the parent page auto-closes the modal and reloads. Legacy path redirects to /destinations (was /settings#library- destinations; e2e test updated accordingly). - New /destinations/modal and /destinations/modal/form routes return the bare body partials for HTMX. - destinationSummaryView gained URL + LastError fields so the new destinations page can show inline error banners. The settings.html "Manage destinations →" link replacing the old inline section lands with the Settings TOC commit.
Two additive changes that share a commit because they're cosmetic finish-out for the last two pages in the DS pass. Diagnostics — adds a "Logs & Environment" tab alongside the existing Drift Inbox (which is unchanged): - diagnostics.html: tab strip at the top, .page wrapper, two tab-panels. New tab has a 2-col grid: live log tail (1024-line ring, 2s poll, level filter, pause toggle) on the left, Environment + Connection-tests cards on the right. - /api/diagnostics/env returns Go version, OS/arch, CPU count, paths, log level, marketplace, auth state, last sync. - /api/diagnostics/logs/tail?n= returns recent lines from the new in-memory ring buffer. - logging.go: 1024-entry ringBuffer teed into outputWriter() via io.MultiWriter. Existing stderr / ConsoleWriter output is unchanged. New TailLogs(n) + RingEntry exported. - logging_test.go: TestOutputWriterMode rewritten to verify the writer end-to-end (writes reach the ring buffer in both JSON and console modes); new TestTailLogsRingBufferOrder covers oldest-first ordering + eviction + n cap. Settings — wraps the existing 990-line template in DS chrome without removing any sections or fields: - settings.html: .page wrapper + .page-header, left-rail sticky .settings-nav with IntersectionObserver scroll-spy. Each existing <div class="settings-section"> tagged with an id/data-section attribute for jumpability — inner markup untouched. - Inline destinations section replaced with a "Manage destinations →" link to the new /destinations page. - New "Re-run Setup Wizard" danger-zone row that POSTs to /setup/restart (for testing the wizard without a full factory reset). Factory-reset description updated to mention the post-reset /setup redirect. /api/diagnostics route registrations shipped with the foundation commit.
Restructures the Pipeline page around DS chrome while keeping all the existing tooling — including Worker Pools, which the DesignSystem mock dropped but the live app needs: - downloads.html: .page wrapper + page-header (Active/Waiting counts in the subtitle), Retry-All-Failed + Queue-pause toggle in the action cluster, the existing queue-paused-banner kept. - Worker Pools section retained above the tab strip — #pipeline-pools is still the target of the SSE renderer in base.html, no JS changes. - Tab strip: Active / Waiting / Failed / Completed. Each tab has an href that server-renders the same panel (so deep links + middle-click open work), with inline JS that toggles panel visibility without a round-trip when clicked. - Active tab keeps #pipeline-cards as the SSE insertion target so the live download cards keep working. Rows are pre-seeded server-side as .dl-card v2 so there's no empty flash before the first SSE tick. - Waiting / Failed tabs use .table-wrap + .tbl v2; per-row Cancel / Retry buttons preserved as HTMX hookups. - Completed tab is now windowed (24h default) with a 7d / 30d / All selector — completed history grows unbounded on a busy install, so scoping the default to "what just finished" makes the page actually useful again. "Showing N of M total completions" exposes the wider totals so users know what bigger windows would reveal. handleDownloads gains ?tab= + ?completed_window= query params. Filter helper filterCompletedByWindow falls back to UpdatedAt when CompletedAt is nil so legacy rows still surface under wider windows.
Catches the sidebar up with the DesignSystem spec. Adds:
- Icons (inline SVGs, no external dep) for each nav link.
- Count badges: Library shows new-book count (muted style), Pipeline
shows active-download count (accent), Destinations shows failed-
destination count (danger). Each badge hides when its count is zero.
- Footer with a health dot, version (pulled from runtime/debug build
info; "dev" in non-release builds), and uptime ("6d 14h").
- Active-link styling now matches the DS: 7px-rounded pill with an
accent left bar.
Server.startedAt set at NewServer construction; computeSidebar runs a
handful of LIMIT-1 counts per full-page render (cheap, and the dash/
pipeline pages refresh their numbers on their own polls anyway).
withSidebar(ctx, data) merges the snapshot into every full-page
render's template data — HTMX fragment responses skip base.html so
they're unaffected.
CSS: .nav-icon, .nav-badge (default/muted/danger), .sidebar-footer
+ .health-dot (with warning variant). Existing .nav-links li a
restyled from block → flex with icon+label+badge layout.
Restyles the book detail modal (book_detail_panel.html) to the DS 2-column layout. Same data, much denser presentation: - Left column: cover image with a stacked action group below (Queue Download / Retry, Convert m4b↔mp3, Delete Files, Open on Audible). All existing HTMX hookups preserved. - Right column: title + series sub-line, status-and-on-disk badge row, .meta-grid <dl> (Author/Narrator/Duration/Released/Purchased/ Publisher/ASIN/Folder), per-destination sync list, files-on-disk <details>, and description with safe-HTML rendering. - Per-destination sync list is new — joins enabled destinations with the per-(book, dest) sync table and renders each row with a chip, display name, and a state badge (Synced/Syncing/Pending/Failed/ Orphaned/Removed). Destinations the book hasn't touched yet show as "Pending" so users can see where the book hasn't landed. handleBookDetail gains BookDestinationStatuses via a new bookDestinationStatuses helper that maps database.BookDestinationSyncState into a small UI vocabulary. CSS: .book-detail switched from flex to grid (220px + 1fr), new .cover-wrap / .cover-actions / .badge-row / .meta-grid / .section-label / .dest-status-list rules.
Brings the standalone delete-confirm page in line with the DS chrome — .page wrapper + page-header, card + .alert err for the warning, DS button variants. The Cancel link now points to /destinations (the new top-level page) instead of /settings#library-destinations. Most users will hit the inline confirm() dialog from the destinations card grid, so this page is mostly a fallback / direct-URL nav path, but it now matches the rest of the app visually.
Edit and Delete were the last full-page navigations in the destinations
CRUD — Edit went to /destinations/:id/edit, Delete went to a separate
confirm page. Both now open the same modal that New already used, which
removes a class of code (page-chrome wrappers around partials, two-state
POST handler for the delete confirm, withSidebar/authBaseData merges for
templates that never had real page context anyway).
Template-side
- destinations_new.html: drop the {{define "content"}} wrapper. Only the
destination_picker_body partial remains, and its action is hard-wired
to /destinations/modal/form (no more PickerAction data field).
- destinations_form.html: drop the content wrapper + drop the $modal
guard (always true now). Form posts via hx-post with X-Dest-Modal=1.
Success body grew a ModalSuccessVerb so the same partial renders
"added" vs "updated" without two near-identical blocks.
- destinations_delete.html: converted from full-page to a
destination_delete_body partial. Confirm body has its own hx-post +
X-Dest-Modal hookup; success body re-uses the same alert.ok pattern.
- destinations.html: Edit + Delete buttons swapped from <a href> and
<form method="POST"> + native confirm() to <button hx-get> that swaps
the modal slot. Toggle stays as a plain POST form because it's a
no-confirm one-click action.
- dashboard_summary.html: "Add destination" link in the section header
and the empty-state CTA now open the modal too.
- base.html: AudplexusDestModal now listens for dest-created /
dest-updated / dest-deleted on the same close-and-reload path.
Handler-side
- Deleted handleDestinationsNewPicker + handleDestinationsNewForm.
Their modal counterparts (handleDestinationsModalPicker /
handleDestinationsModalForm) were already doing the same work.
- handleDestinationsModalPicker / handleDestinationsModalForm: drop
PickerAction / ModalMode plumbing; the partials don't read those
any more.
- handleDestinationEditForm: returns destination_form_body directly
(no withSidebar, no page chrome). Same partial as Add — $isEdit
inside the template flips action + button labels when .Dest is set.
- handleDestinationUpdate: forks on success like handleDestinationsCreate
— HX-Trigger dest-updated + modal success body. Error paths re-render
destination_form_body with FormError set instead of crashing into
renderAuthPage.
- handleDestinationDelete: split into two handlers. New GET
/destinations/:id/delete returns the confirm body. POST stays as the
actual delete, requires confirm=1 (the modal sets it), forks on
success with HX-Trigger dest-deleted.
Routes
- /destinations/modal + /destinations/modal/form (add picker + form).
- /destinations/:id/edit → GET form body (modal).
- /destinations/:id → POST update.
- /destinations/:id/delete → GET confirm body, POST delete (with
confirm=1).
- Legacy GET /destinations/new now 301s to /destinations so old
bookmarks land somewhere useful.
CSS
- New .link-btn helper for <button>s that should look like inline
links (used by the dashboard "Add destination" affordances now that
they fire HTMX instead of navigating).
Brings the Settings page in line with the rest of the DS pass. Every setting, form field, button, and JS hook is preserved — only the chrome changed. Structure - Each section is now a .card with .card-head instead of the old .settings-section flat-with-h2 look. Section IDs (#section-auth, #section-system, #section-config, #section-devices, #section-danger) kept so the scroll-spy TOC still works. - Auth & Integrations: two .card panels side-by-side via the new .auth-section grid — "Amazon / Audible" (region select + sign-in flow) and "Tag Profile". The legacy "Library Destinations" link-out card was removed; the sidebar already has the Destinations link, so it was redundant. - System Information: collapsed to a single .card with a .meta-grid <dl> (same pattern as the book detail modal). All 4 paths preserved. - Configuration: .form-row pattern throughout, .form-row.inline for the 5 toggles (sync_enabled, sync_auto_queue_new, embed_cover, chapter_file, plexmatch_file) using the DS .toggle component. The 3 long template fields (author_dir_template, book_dir_template, file_name_template) span the full grid width. Save / Reorganize buttons + #save-status anchor kept. - Registered Devices: .table-wrap + .tbl v2. - Danger Zone: .card with an opening .alert err warning, danger-action rows preserved. btn-outline → btn-outline-ds for the Download Backup button. Modals - Restart and Factory Reset modals switched from the bespoke .modal-box to the global .modal-overlay + .modal-dialog chrome. Same IDs (#restart-modal, #reset-modal, #final-confirm, #reset-confirm-input, #restart-confirm-btn, #restart-modal-status) so the existing modal- driver JS keeps working without changes. Click-outside backdrop + × close button added. CSS - New DS form primitives added to style.css: .input / .select / .textarea, .checkbox, .toggle + .toggle-slider, .form-row + .form-row.inline + .form-row .hint, .card-head, .code block. These were already referenced by setup.html (the wizard) so they get proper styling there too as a bonus. - Bottom scoped <style> block in settings.html went from ~450 lines to ~55. Everything that duplicated global rules was pruned; what remains is the Settings-only layout primitives (auth-section grid, settings-form-grid, danger-action rows, btn-inline used by settings_saved.html).
The dest-created / dest-updated / dest-deleted HX-Trigger headers
were built by string concatenation with user-controlled DisplayName
spliced directly into JSON:
c.Header("HX-Trigger", `{"dest-created":{"id":"`+d.ID+`","name":"`+d.DisplayName+`"}}`)
A DisplayName containing `"` would break the JSON; a crafted value
like `Plex","extra-event":{"x":""` could inject sibling events.
HTMX parses the payload and dispatches by key, so impact is limited
to firing extra DOM events on document.body, but it's a real
correctness bug.
Routed all three call sites through a new setDestModalTrigger helper
that marshals via encoding/json. Event names stay server-controlled
literals; payload keys/values go through proper JSON encoding.
After the modal-only refactor (df5764e), the non-modal error branches in handleDestinationsCreate were unreachable: every UI entry point posts with X-Dest-Modal=1, and the renderAuthPage calls rendered the old "Authentication & Integrations" settings page with an Error field — a layout that no longer exists since destinations split out of Settings. Replaced both non-modal error branches with plain c.String() so direct POSTs from curl/scripts still get a usable response. Modal branches gained no behaviour, just dropped the now-dead ModalMode data field and threaded c.Request.Context() once at the top instead of repeating the call. handleDestinationUpdate already used the right shape; only the template-side ModalMode field was stale, removed in the same sweep. Tests still green (e2e test exercises the non-modal happy-path redirect which keeps working).
handleDestinationToggle used to redirect to /settings#library- destinations on success — but the destinations split-out removed that anchor when the inline destinations block was replaced with a link-out button. Users clicking Enable/Disable were dropped on /settings instead of bouncing back to where they came from. Two changes: - Extracted the dest-card markup into destination_card_body so the same partial renders the card on the full grid AND as the response to a single-card mutation. New singleDestinationSummary(ctx, id) helper builds the view-model for one row, sharing the existing buildDestinationSummary logic between bulk and single paths. - handleDestinationToggle now branches on HX-Request: HTMX callers get the rebuilt card body back (button's hx-target is its own card; outerHTML swap); plain POSTs still get a redirect, now pointed at /destinations. Side effect: clicking Enable/Disable is instant and in-place — no full page reload, no jumping to a different route. Cards that flip to Disabled lose their item-count/coverage probe (the helper skips expensive backend calls for disabled rows) so the swap stays cheap.
… gate Closes the gap noted in the post-DS review: ~1500 lines of new web code shipped without unit tests on the pure helpers. Adds: - TestPaginationNumbers — 7 cases covering small-total (no ellipsis), current near start/middle/end of large total, and the edge cases current=1 / current=total. Asserts exact slice equality via reflect.DeepEqual so a refactor that drops or reorders ellipsis markers fails loudly. - TestParseCompletedWindow — 8 cases. Critical assertion: any unrecognized input (empty, gibberish, "1h", etc.) falls through to the 24h default. A casual click on /downloads?completed_window=foo must never silently surface all-time history. - TestFilterCompletedByWindow — builds rows at known offsets relative to time.Now() and asserts which ASINs survive each window (24h / 7d / 30d / all). Covers the legacy-row fallback path where CompletedAt is nil and UpdatedAt is used instead. - TestFirstRunGate — table-driven coverage of the gate's path-allowlist + method-allowlist. Verifies that the 4 gated GETs (/, /library, /downloads, /destinations) 303 to /setup when onboarded != "1", and every other route (settings, diagnostics, /api/*, /static/*, all POSTs) passes through. Separate TestFirstRunGate_OnceOnboarded confirms the gate passes everything through once the flag is set. Adds a tiny sameStringSet helper used by the filter test so the order of survivor rows doesn't get baked into the assertion.
libraryStatusCounts was firing 7 LIMIT-1 COUNT(*) queries per library page render — one per status the filter tabs care about. Replaced with a single CountBooksByStatus that returns a map[BookStatus]int from a single GROUP BY query. - New CountBooksByStatus on the Database interface, implemented on SQLiteDB and PostgresDB with the same SELECT status, COUNT(*) FROM books GROUP BY status query. StubDB returns nil to satisfy the interface for unit tests that don't touch books. - libraryStatusCounts in web/server.go simplified to a single switch-map; the "all" bucket sums every status' count so it matches the legacy total (which used a no-filter LIMIT-1 count). Skipped status contributes to "all" but no tab, matching legacy behaviour. - New TestCountBooksByStatus in internal/database covers the empty table case and a seeded mix across 4 statuses. Stub's interface- completeness test also gained an assertion on the new method so adding a future status doesn't silently slip past the stub.
… read
firstRunGate runs on every GET. Each call was hitting GetSetting →
SQL query for the "onboarded" key — a value that flips maybe twice in
the app's lifetime (once on first finish, once if the user re-runs
the wizard). Cheap on SQLite but pure waste.
- Added Server.onboarded (atomic.Bool). Seeded from the DB exactly
once in NewServer; flipped by the new setOnboarded(ctx, value)
helper which persists to the DB and mirrors to memory in lockstep.
- isFirstRun now reads s.onboarded.Load() — no allocation, no
syscall, no SQL. ctx kept in the signature for symmetry but the
read doesn't need it.
- All four mutation sites updated:
- handleSetupFinish → setOnboarded(true)
- handleSetupSkip → setOnboarded(true)
- handleSetupRestart → setOnboarded(false)
- handleFactoryReset → s.onboarded.Store(false) (DB is wiped
by db.Reset, atomic mirrors that)
- TestSetOnboarded covers the round-trip: atomic and DB stay in
sync across true→false→true transitions. TestFirstRunGate_
OnceOnboarded updated to go through setOnboarded instead of
poking the DB directly, since direct DB writes no longer affect
what firstRunGate sees.
The Database-interface addition in 7febaf2 added CountBooksByStatus on the production impls (SQLiteDB/PostgresDB) and the canonical testutil_stub.go StubDB, but three additional test-only stub types in other packages also implement the interface and broke the build: - internal/mediaserver/backend_contract_test.go → settingsOnlyStubDB - internal/mediaserver/destination_ids_test.go → destinationIDsStubDB - internal/library/library_reconcile_test.go → reconcileMockDB Each gets a no-op CountBooksByStatus returning (nil, nil) — these stubs aren't exercised by code that reads book counts, so a nil map is the right shape (libraryStatusCounts treats it as "no books"). Pre-existing Windows path-separator test failures (TestFileValidator_ ExtractASIN, TestRenderNamingTemplateCustomLayout) remain — those are unrelated and predate the DS refresh.
Introduces a new POST `/api/books/:id/resync-metadata` endpoint and handler. This allows users to manually refresh a book's metadata (e.g., title, author, cover URL, description, series) from Audnexus without impacting local files or download status. A "Re-sync metadata" button is added to the book detail panel, triggering this action via HTMX with an automatic page reload on completion. Also includes minor UI refinements: adding SVG icons to various buttons in the book detail panel and a reorganization of the settings navigation links and their corresponding sections for better clarity. ```
The book detail modal (HTML, JS, and CSS) is extracted from `library.html` into its own reusable partial `book_detail_modal.html`. This component is now included in both `/library` and `/dashboard` pages. Book links on the dashboard's tables (e.g., recent completions/failures) now open details within this modal, providing a consistent user experience and preventing full page reloads. The modal also supports `?book=` deep linking and history manipulation. New `.btn-icon` styles are introduced and applied to action buttons on destination cards in the dashboard summary.
Introduce robust error message cleaning for `last_health_check_err` fields, stripping HTML, collapsing whitespace, providing hints for common HTTP errors, and truncating long messages. This ensures display-friendly output in UI badges and test results. Also, implement a global HTMX-driven mechanism for `data-test-button`s that provides dynamic loading, success, and failure states with visual indicators (spinner, checkmark, cross). This replaces manual `hx-on` attributes for test buttons, centralizing the logic. Finally, add styling for disabled destination cards, making their status visually distinct on the dashboard and detail pages.
Introduce a new template function `destLogo` and corresponding static image assets to display custom logos for Plex, Emby, Jellyfin, and Audiobookshelf destinations in the UI. This enhances visual distinction and user experience.
Implement a new design system for destination forms, featuring an identity header, sectioned layout with eyebrow dividers, guided stepper for Plex setup, consolidated help disclosures, and a sticky action bar. This significantly improves the user experience and visual consistency of these critical forms, including a safer, type-to-confirm flow for deletion.
The Audible API's `/library` endpoint requires the `relationships` response group to return the `purchase_date`. Without this, the 'Purchased' date would appear blank on the Library page. This change explicitly requests the `relationships` group when fetching the Audible library, ensuring the purchase date is available for display.
Improve the library sync status panel to provide comprehensive information, even when no sync is actively running. This includes: - Displaying a summary of the last completed sync (time, books found/added, status, error). This data is sourced from in-memory state or a database fallback on cold start. - Projecting and displaying the next scheduled sync time based on configured settings, including handling disabled auto-sync or invalid schedules. - Introducing a new `formatRelative` template helper for human-friendly time strings. These changes ensure the sync panel is always informative, giving users better visibility into their library's synchronization history and future schedule.
Introduces a `cleanError` template helper to standardize the rendering of error and detail strings across the dashboard and sync status pages. This helper strips embedded HTML, collapses whitespace, rewrites common HTTP codes, and truncates messages for consistent and user-friendly presentation.
Introduces a one-shot log entry for the first book found during an Audible sync to inspect its `purchase_date` and `release_date` fields. This is to verify if the API consistently populates these fields with the SDK's default response groups.
Audible's API is inconsistent with the date formats returned for `purchase_date` and `release_date`. Fields can come back as RFC3339Nano, RFC3339, or simple ISO date, and can occasionally be empty. This change introduces a new `parseAudibleDate` helper function that attempts to parse these fields using a prioritized list of known layouts, ensuring correct interpretation across all observed formats. The one-shot diagnostic log, previously added to investigate these date fields, is also removed as the issue has been understood and addressed.
A new 'Connection Tests' panel provides a centralized overview of Audible API and configured destination health, complete with inline test buttons. The 'Live Logs' panel gains client-side functionality to copy individual log lines or all visible log messages, improving debuggability. For Plex destination editing, the guided stepper now pre-seeds its state from existing configuration, streamlining the editing experience. Applies the shared error message cleaner to destination health checks in the diagnostics comparison view for consistent, user-friendly error display.
The Plex guided setup's Step 3 (select library) could incorrectly display as "done" or healthy when: - The destination's health was never checked (`LastHealthCheckOK` was `nil`). - A previously saved Plex section ID no longer exists on the Plex server. This change prevents these misrepresentations by: - Requiring an *affirmative* "OK" result from the last health check for Step 3 to be considered done. - Introducing client-side logic to verify the saved section ID against the list of sections discovered from the Plex server. If the stored ID is stale or invalid, it's cleared, the step is marked with an error, and a message prompts the user to select a new library. This provides more accurate feedback and prevents users from saving destinations with invalid Plex section configurations.
This change centralizes error message sanitization into a new `errs.CleanForDisplay` helper. It transforms raw backend errors (which often embed HTML or verbose upstream responses) into concise, user-friendly strings. Capabilities include stripping HTML, collapsing whitespace, providing hints for common HTTP status codes, and truncating very long messages. The cleaner is applied to sync progress messages and destination health checks. This guarantees consistent, plain-text error display across the UI, preventing layout issues (e.g., from raw HTML). It also refactors existing error cleanup in the web package to use this shared utility. Additionally, CSS for live log rows is updated to make the log copy button float over messages without reserving layout space, improving the visual stability of the diagnostics panel.
Replaces the simple 'On Disk' indicator with a comprehensive 'Presence' column in the library table. This new column shows whether a book is available locally and its synchronization state (synced, syncing, failed, pending) with each configured external destination. It offers an at-a-glance overview of book distribution and sync health.
Books matched against ABS during reconcile kept sync_state='pending' forever, so the UI badge said "Pending" on every book even though server_item_id was populated. Reconcile proves the destination has the item — promote pending→synced (preserving failed/orphaned which carry signal).
Reconcile previously filtered local books to status=complete, so books in any other status would never get matched against ABS items by ASIN even when ABS plainly had them. Drop the filter — match all books. Also add a folder-path ASIN fallback for ABS items whose metadata.asin is empty: audplexus's download layout embeds the ASIN in folder names (e.g. "Title B0CJS1PS67 [us]"), so we can recover the match when ABS's own metadata extraction missed it. Log unmatched and no-ASIN counts at reconcile end so future drift is visible instead of silently skipped.
…ping Books that fail Audible's CanDownload check (e.g. once-Plus-catalog titles the user no longer has access to) were silently skipped during sync, leaving a gap between the Audible library count and what audplexus shows. Persist them as status=unavailable with a cleaned-up reason string so the UI can surface why they're missing. - New BookStatus 'unavailable' and books.unavailable_reason column (migration 007 for both sqlite and postgres). - On subsequent syncs, re-check unavailable books in case the user regained access (Plus title added back). - errs.CleanEntitlementReason distills Audible's verbose multi-clause license-denied error into a short user-facing reason (e.g. "No longer included with your Audible plan").
- New "Unavailable" tab and dropdown option on the library page, with a count badge fed by libraryStatusCounts. - Warning-triangle icon next to the title in the library row, with the cleaned reason in its tooltip. - Inline warning notice in the book detail panel explaining why the title can't be downloaded. - Unavailable status uses the warning badge color so it's visually distinct from new/complete/failed.
Contributor
Author
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.

This is a big one, While doing the UI refresh I kept hitting visible bugs that were lurking in the sync and matching paths. There are a few elements I am not fully happy with, but This PR is already large enough and can be taken care of in follow up passes.
Onboarding wizard, This can be re-ran at any time on the settings screen:

Dashboard:

Destinations split out from Settings screen:

Library, now with pagnation:

Book Details:

Try it
CI builds are enabled on this branch — pull the latest image with:
Bugs fixed along the way
Rather than ship a pretty UI on top of broken plumbing, I fixed some bugs along the way:
CanDownloadcheck (e.g. once-Plus-catalog titles you no longer have access to) werecontinue'd past without ever hitting the DB, leaving a gap between your Audible library count and what audplexus showed. Now persisted asstatus=unavailablewith a cleaned reason, surfaced under a new Unavailable tab with a warning triangle and inline explanation.server_item_id, butsync_statestayedpendingforever — nothing promoted it. On my server, that meant 607 books all displayed "Pending" despite being fully synced. Reconcile now promotespending → syncedwhen a match lands.status=completebooks. Anything innew/queued/failed/etc. was invisible to the matcher even when ABS had it. Filter dropped — now matches every book by ASIN.metadata.asinwere silently skipped. ABS's own audible match misses about 3% of items in my library. Added a folder-path ASIN fallback (Title B0CJS1PS67 [us]style) and counters in the reconcile log so future drift is visible instead of silent./libraryissued 7 separateLIMIT 1count queries to populate the status-tab badges. Collapsed to a singleGROUP BY.atomic.Bool.renderAuthPagebranch and anHX-Triggerpayload that hand-built JSON (nowencoding/json).Databaseimplementations didn't satisfyCountBooksByStatus— added test coverage.What's new
First-run onboarding. New users land on a guided wizard (devices → first sync → first destination) instead of a blank dashboard. The "onboarded" gate redirects out automatically once setup is done.
Design-system refresh across every page. New foundation primitives (
chore(ui): foundation primitives for DS chrome) underpin a refreshed dashboard, library, pipeline, settings, destinations, diagnostics, and book-detail modal. Highlights:Plex guided destination form. Step-by-step Plex setup with section-selection validation and live edit experience instead of a flat form.
Manual book metadata re-sync. Per-book button that re-fetches from audnexus and updates fields in place without touching the file.
Shared error display helper (
internal/errs). Strips HTML, collapses whitespace, rewrites common HTTP status codes into hints, truncates noisy upstream bodies. Used everywhere errors surface — destinations health, sync phases, dashboard recap, drift inbox — so the same failure always reads the same way. Extended withCleanEntitlementReasonfor Audible's verbose multi-clause license-denied messages.