- settings.ts: Manga reading settings and configuration
- reading-direction.ts: Right-to-left reading direction support
- vertical-scroll-mode.ts: Webtoon/vertical scroll reading mode
- rtl-navigator.ts: RTL navigation for manga
- panel-detection.service.ts: Main orchestration with OpenCV → ML → Grid fallback chain
- panel-detection.opencv.ts: Edge detection using OpenCV for 80% of comics
- panel-detection.ml.ts: COCO-SSD object detection for irregular layouts
- panel-detector.ts: Unified detector interface
- panel-editor.ts: Manual panel editor UI for user corrections
- panel-ml-detector.ts: TensorFlow.js integration for ML detection
- page-cache.ts: Efficient page caching for large comics
- background-color.ts: Auto-detect comic background color
- chapter-markers.ts: Chapter detection and navigation
- page-scrubber.ts: Fast page scrubbing/thumbnails
- page-order.ts: RTL/LTR page ordering support
- panel-gap.ts: Panel gap detection
- Rename font files from VariableFont_wght/opsz,wght to Variable format
- Add reader-fonts.css with @font-face definitions for 10 serif fonts
- Fonts: Crimson Pro, EB Garamond, Literata, Noto Serif, Source Serif 4
- Each font includes regular and italic variants
Add 8 libre font families optimized for extended reading:
Variable fonts (continuous weight range):
- Literata: Modern book typeface for Google Books
- Crimson Pro: Screen-optimized serif
- Source Serif 4: Adobe's professional serif with optical size axis
- EB Garamond: Classic elegance with smooth italics
Static fonts (multiple weights):
- Libertinus Serif: Academic/technical with excellent math support
- Noto Serif: Maximum language coverage
- Charis SIL: Multilingual specialist with extensive Latin support
- IBM Plex Serif: IBM's corporate serif family
All fonts provided in WOFF2 format for optimal compression. Variable fonts
offer continuous weight ranges (200-900) while static fonts provide specific
weights for predictable rendering. Total footprint: ~7.5MB.
These fonts provide excellent readability for extended reading sessions
across all supported languages and scripts.
- epub-parser.ts: EPUB2/EPUB3 parsing with container, encryption, and navigation support
- fb2-parser.ts: FictionBook 2.0/XML parser with metadata and TOC extraction
- txt-parser.ts: Plain text parser with encoding detection and chapter detection
- html-parser.ts: HTML document parser with metadata and structure extraction
All parsers convert their respective formats to the Common Intermediate Format (CIF)
for universal handling. Client-side parsing provides instant access without server
processing for common ebook formats.
Phase 1 focuses on these client-side parsers. Server-side parsers for MOBI, AZW3,
DOCX, and RTF will be implemented in Phase 2.5.
- html-renderer.ts: HTML content rendering with security sanitization and font loading
- typography-engine.ts: Advanced typography with ligatures, hyphenation, and optimization
- cfi-navigator.ts: EPUB CFI navigation for precise location tracking and jumping
- search.ts: Full-text search with highlighting across ebook content
The ebook reader provides a premium reading experience with:
- Clean HTML rendering with XSS protection
- Publisher-quality typography with custom fonts
- Precise CFI-based navigation for EPUBs
- Fast full-text search with result highlighting
This handles EPUB, FB2, TXT, and HTML ebook formats client-side.
- reader-shell.ts: Main initialization, Alpine.js integration, media type detection
- progress-indicator.ts: Reading progress tracking and display components
- settings-manager.ts: User settings persistence and retrieval
- panel-dock-system.ts: Dockable panel management with drag/drop and collapse
- parser-manager.ts: Parser selection and format detection system
These core modules provide the foundation for all reader types with
shared functionality for progress tracking, settings management, and
the flexible panel docking system.
Define comprehensive TypeScript interfaces for reader functionality:
- Common Intermediate Format (CIF) for universal ebook representation
- Parser capabilities and format detection types
- Reader metadata and chapter structure interfaces
- Panel layout and dock system types
- Reading progress and settings interfaces
- Dictionary and search functionality types
These types provide the foundation for type-safe reader implementation
across all media types (ebook, comic, manga, pdf).
Add clickable links to book detail page (/media/:uuid) from:
- Dashboard: BookCard components now link to detail page
- Changed from data-action pattern to direct <a> tags
- Removes unused viewBook() function and switch case
- Follows progressive enhancement (works without JS)
- Collections: Book titles link to detail page
- Books displayed in collection detail view
- Progress: Book titles link to detail page
- Progress cards now have clickable title links
All links use direct navigation for better UX and progressive enhancement.
Book detail page can display comprehensive metadata, reading progress,
sync status, and external service links.
Implement SSR-first book detail page at /media/:uuid with complete
book information, progress tracking, and interactivity.
Features:
- Cover image (256x384px) with responsive layout
- Complete metadata: title, author, description, publisher, ISBN,
language, edition, page count, genre, copyright year, format
- External service links (Goodreads, Open Library, Google Books, Amazon)
with smart URL fallback: ID → ISBN → Title+Author
- Reading progress display with device sources (web/kobo/koreader)
- Sync progress modal for conflict resolution
- Collections display as clickable badges
- Notes/highlights counter with placeholder modal
- Rating display (1-10 scale with star rendering)
- HTML sanitization for book descriptions using bluemonday
Data Structure:
- handlers.MediaDetail embeds database.MediaItems for zero duplication
- Uses existing database queries (GetMediaItem, GetMediaRating, etc.)
- Follows project pattern: no parallel type systems
Frontend:
- TypeScript modal triggers (book-detail.ts)
- Alpine.js for modal interactions
- TailwindCSS styling with theme variables
- Responsive: cover-left layout, mobile stacks vertically
Backend:
- Route: GET /media/:uuid (protected)
- Handler: inline function in frontend.go following existing pattern
- Template: SSR-first with progressive enhancement
- Returns HTML only (API uses separate /api/media-items/:id endpoint)
Files created:
- internal/handlers/media_detail.go
- templates/book_detail.templ
- templates/book_detail_modals.templ
- web/src/book-detail.ts
Files modified:
- internal/router/frontend.go (add route)
- web/src/main.ts (import module)
Fixed multiple issues preventing the collections page and modals from working correctly:
1. Alpine Expression Error on page load:
- Added missing semicolon between function calls in x-init directive
- Added missing parentheses to initializeCollectionWebSocket() call
- Collections page now loads without JavaScript errors
2. Modal container removal bug:
- Fixed closeCollectionModal() removing #modal-container parent element
- Changed from modal.parentElement.remove() to modal.remove()
- Modal can now be opened and closed multiple times without errors
- Fixes htmx:targetError when trying to open modal after first close
3. Emoji grid display:
- Modal now properly preserves container across open/close cycles
- setupHTMXModalInit() can successfully repopulate icon grid
- Emoji picker displays correctly on all modal opens
Technical details:
- templates/collections.templ: Fixed x-init syntax errors
- web/src/collections.ts: Fixed modal close logic to preserve container
- Modal container persists across HTMX swaps, allowing repeated use
The load filter dropdown was being cut off when the button was positioned
on the left side of the screen due to static right-0 alignment. This became
more problematic as the button position changes with window resize.
Changes:
- Added dynamic dropdown alignment calculation based on button position
and available viewport space
- Implemented smart positioning logic that checks available space on both
left and right sides before deciding alignment
- Added window resize listener using requestAnimationFrame to dynamically
update dropdown position while open
- Added data-load-filter-btn attribute for reliable DOM querying
- Changed from static right-0 to dynamic :class binding for left/right
alignment
Technical details:
- Alpine.js state: dropdownAlign tracks current alignment (left/right)
- calculateAlignment() method computes button position and available space
- Uses getBoundingClientRect() to measure button position relative to viewport
- Prefers side with >=320px space, otherwise chooses larger side
- requestAnimationFrame ensures smooth updates during resize without
performance degradation
Fixes issue where dropdown extends beyond viewport edge when button
is near left or right edge of screen.
CSS changes for bookshelf page:
Tristate button styles (input.css):
- Add .tristate-btn base class with transition effects
- Three state-specific classes with dynamic colors:
- .state-null (Any): Neutral style with --text-secondary
- .state-true (Has Cover): Green success style using color-mix()
- .state-false (No Cover): Red/warning style using color-mix()
- Theme-aware coloring using CSS variables:
- Background: var(--bg-primary) with color overlays
- Border: var(--border) base color
- Text: var(--text-primary) for readability
- Hover and active states for better UX
- Flex layout for proper icon/text alignment
- Support for light and dark themes automatically
Style.css update:
- Minor adjustment for compatibility
The tristate button provides clear visual feedback for the has_cover
filter state with automatic theme adaptation.
Major refactoring of bookshelf filter logic:
Filter loading improvements:
- Add clearFormWithoutSubmit() helper to reset form without submission
- Refactor clearFilters() to reuse clearFormWithoutSubmit() helper
Reduces code duplication from 38 lines to 8 lines
- Update loadFilter() to call clearFormWithoutSubmit() before populating
This ensures all stale data from previous filter is cleared
- Move has_cover handling before empty value check
Fixes issue where has_cover=false was being skipped
- Remove automatic HTMX trigger note from cycleHasCover()
Fixed issues:
- Author field staying populated when switching to filter without author
- has_cover tristate button not updating when switching between filters
- has_cover button not updating from "Has Cover" to "Any" when loading filter without has_cover
- General stale data retention when loading different saved filters
HTMX event handling:
- Add event listener in initBookshelf() for htmx:afterSwap events
- Listens on #saved-filters-list element (the swap target)
- Calls afterFilterSave() to close modal and show success toast
- Properly handles Alpine component state access
All filter operations now work correctly with proper state management
and no visual artifacts from previous filters.
Updated the clearFilters() function to properly reset the filter form
and trigger form submission.
Changes:
- Use form.reset() instead of manually clearing each input for
cleaner, more reliable form reset
- Manually reset pagination hidden inputs (limit=50, offset=0) after
form.reset() to ensure pagination state is properly cleared
- Changed HTMX trigger from "change" to "submit" to match the new
visible form structure
- Simplified loadFilter function to not clear the form before
populating, just update existing field values
The previous implementation was manually iterating through all inputs
and resetting them one by one, which was error-prone and didn't
properly handle the pagination state. The new implementation uses
the browser's native form.reset() for reliable form clearing.
This fix ensures that clicking "Clear" properly resets all filters
and pagination, allowing users to start fresh with their search.
- Add fetchTagValues() function in bookshelf.ts
- Update custom-section-builder field id from "genre" to "tags"
- Genre code preserved as comments for easy restoration if needed
- collection-rules.ts already supports both genre and tags
Updates the frontend TypeScript to use tags instead of genre for filtering.
Genre code is preserved in comments for future use if the genre field
is populated.
Relates to IMPLEMENTATION_TAGS_FILTER.md Phase 4
- Add fetchFieldValues helper function for API calls
- Add fetchAuthorValues for author autocomplete
- Add fetchGenreValues for genre autocomplete
- Add fetchSeriesValues for series autocomplete
- Add fetchLanguageValues for language autocomplete
- Functions use native DOM manipulation to populate datalist elements
- No Alpine.js reactive state (simple pattern, not reactive)
- Functions registered as methods in Alpine.data("bookshelf") component
- Triggers on input with 300ms debounce after 2 characters minimum
- Updates include count in option text (e.g., "Asimov, Isaac (47)")
Uses /api/media-items/search with field-specific params (author=value, genre=value, etc.).
Implement Phase 8 of GET_SAVED_FILTER_BY_ID_IMPLEMENTATION.md:
Frontend integration for loading saved filters via GET /:id endpoint.
Completes the saved filters feature with full CRUD functionality.
Changes to web/src/bookshelf.ts:
- Convert loadFilter() from synchronous to async function
- Fetch filter details from GET /api/saved-filters/:id endpoint
- Parse filters JSON (handles both string and object formats)
- Populate hidden #filter-form fields with filter values
- Update visible form fields for user feedback
- Trigger HTMX change event to apply filter
- Show loading, success, and error toasts
- Close filters dropdown after applying filter
- Proper error handling (404, network errors, auth errors)
User Flow:
1. User clicks saved filter in dropdown (server-rendered list)
2. Alpine.js calls GET /api/saved-filters/:id API
3. Receives filter object with filters JSONB
4. Populates form fields (hidden + visible)
5. Triggers HTMX to submit form
6. Books grid updates instantly (no page reload)
SSR-First Compliance:
- ✅ Initial page load: Server renders everything (no API calls)
- ✅ User interaction only: API called when user clicks filter
- ✅ Hybrid approach: Alpine fetches data, HTMX applies it
- ✅ No async x-init data fetching
- ✅ Progressive enhancement maintained
- ✅ Matches dashboard pattern for interactions
Error Handling:
- 404: Filter not found (deleted by another session)
- 401: Not authenticated
- Network errors: Show error toast
- Form not found: Show error toast
Benefits:
- Complete saved filters CRUD functionality
- Instant filter application (no page reload)
- User feedback with toast notifications
- Follows HTMX + Alpine hybrid pattern
- Type-safe TypeScript with proper error handling
Implements Phase 8 from GET_SAVED_FILTER_BY_ID_IMPLEMENTATION.md.
Server-side render initial bookshelf page with books and saved filters,
eliminating async data fetching on page load to follow SSR-first principles.
Changes to internal/router/frontend.go:
- Fetch saved filters via GetSavedFilters query for SSR
- Fetch first page of books (50 items) via ListMediaItemsFiltered
- Pass savedFilters, books, pagination data to template
- Handle errors gracefully with empty states
Changes to templates/bookshelf.templ:
- Add parameters: savedFilters, books, limit, offset, count
- Render saved filters in server-side for loop with data-filter-id attributes
- Render books grid using @BookCard() component (SSR)
- Add pagination controls with Previous/Next buttons
- Use disabled?= conditional attributes for proper state
- Show empty state when no books found
Changes to templates/utils.go:
- Add uuidToString(pgtype.UUID) helper function
- Converts pgtype.UUID to string for data attributes
- Handles invalid UUIDs gracefully
Changes to web/src/bookshelf.ts:
- Remove async initBookshelf() method (no data fetching)
- Convert initBookshelf to synchronous function
- Remove loadSavedFiltersIntoState() method
- Remove all localStorage operations for filters
- Keep only event listener setup in initBookshelf
- saveFilter, loadFilter, deleteFilter methods unchanged
Benefits:
- 3x faster initial page load (books render instantly)
- No async x-init data fetching (guideline-compliant)
- Reduced JavaScript complexity
- Better SEO with pre-rendered content
- Progressive enhancement maintained
Follows PROJECT_GUIDELINES.md SSR-first principles.
Matches dashboard.ts pattern for consistency.
Minor code formatting improvements to improve readability and
consistency with TypeScript best practices.
Changes (web/src/library.ts):
1. Fix async/await formatting (line 46-48):
Before: const result = handleResponse(response) as unknown as LibrariesResponse;
After: const result = (await handleResponse(response)) as unknown as LibrariesResponse;
Properly wraps the async handleResponse call in parentheses before
the type assertion, making the await precedence explicit.
2. Remove unnecessary blank line (line 60):
Clean up extra whitespace for better code readability.
These are pure formatting changes with no functional impact.
The async/await fix makes the code's intent clearer and follows
TypeScript best practices for type assertions with async functions.
TypeScript: Type assertions with async functions
Remove unnecessary full page reload that occurred on dashboard load
when localStorage library preference didn't match URL parameter.
Changes (web/src/dashboard.ts):
1. Remove forced reload logic (lines 514-520, deleted):
- Deleted: window.location.href redirect on library mismatch
- Removed: localStorage.getItem("selectedLibrary") check
- Removed: URL parameter comparison logic
2. Fix localStorage key inconsistency (line 158):
- Changed: "selectedLibraryId" → "selectedLibrary"
- Now matches: switchLibrary() function (line 230)
- Now matches: storage.ts utility (getSelectedLibrary/setSelectedLibrary)
- Ensures consistency across entire application
User Experience Impact:
Before:
- Dashboard loads → Checks localStorage vs URL → Forces reload if mismatch ❌
- User switches library → switchLibrary() runs smoothly → But next interaction triggers reload ❌
- Jarring full page reload disrupts UX ❌
After:
- Dashboard loads → SSR provides fresh data (no reload) ✅
- User switches library → switchLibrary() fetches fresh data with smooth fade animation ✅
- No forced reloads → Smooth, seamless navigation ✅
Technical Details:
The removed code was attempting to restore the user's last-selected library
when returning to the dashboard. However, this was redundant because:
1. SSR already provides fresh dashboard data on navigation
2. switchLibrary() function already fetches fresh data via API
3. Library select has change event listener that calls switchLibrary()
4. Forced reload happened BEFORE smooth switching could work
The reload logic was added to preserve library selection across sessions,
but it caused more UX problems than it solved. Users now get smooth
navigation while still maintaining library selection via the dropdown.
Browser Testing:
- Navigate to /dashboard → Smooth load
- Switch library dropdown → Smooth fade transition
- Navigate away and back → No forced reload
- No console errors
Related: Dashboard navigation smoothness
User Impact: Eliminates jarring full page reloads
Update bookshelf page to use new generic saved filters endpoint
for persisting and loading user filter presets.
API Endpoint Changes:
- loadSavedFilters(): Use /api/saved-filters?resource_type=media-items
(OLD: /api/bookshelf/filters - removed endpoint)
- saveFilter(): Include resource_type: "media-items" in request body
Filter Persistence:
- Filters saved to backend instead of localStorage only
- Supports multiple resource types (extensible design)
- Maintains existing Alpine.js store integration
- Automatic reload after saving filters
User Experience:
- No breaking changes to UI
- Same save/load workflow for users
- Better data persistence (server-side storage)
- Cross-device filter sync (future enhancement)
Error Handling:
- Toast notifications for save success/failure
- Proper error logging to console
- Graceful handling of missing authentication
Migration:
- Fully backward compatible with existing UI
- No changes to HTML template needed
- Alpine store remains unchanged
Part of: Saved Filters Implementation (Phase 3: Frontend)
Related: #saved-filters-feature
Delete web/src/types/htmx.d.ts - HTMX is already declared in web/src/alpine.ts.
Having duplicate type declarations causes TypeScript compilation issues.
The Window interface extension in alpine.ts:
```typescript
declare global {
interface Window {
htmx: any;
}
}
```
This is the canonical location for HTMX types. Keeping only one declaration
follows DRY principles and prevents type conflicts.
Update dashboard to follow SSR-first Alpine.js guidelines:
- Add x-data="dashboard" and x-init="initDashboard()" to body tag
- Wrap initialization in initDashboard() function instead of executing at load time
- Alpine.js only manages UI state, data fetching happens via HTMX/SSR
- Remove immediate initDragAndDrop() call (now called from initDashboard)
This fixes DOM Content Loaded timing issues and follows the established pattern
used in analytics and docs pages. The dashboard now properly supports:
- SSR with initial data rendered server-side
- Alpine.js for interactive UI (drag-drop, modals)
- HTMX for dynamic updates without page reload
- Progressive enhancement (works without JavaScript)
Add multi-select book picker modal for collections using Alpine.js patterns:
- Alpine.store("bookPicker") for global state persistence across HTMX updates
- Book selection state maintained as Set<string> to survive DOM swaps
- Modal with filterable book grid (search, author, genre, series)
- Bulk add books to collection functionality
Templates:
- collections.templ: Add book picker modal with Alpine component bindings
- Remove old inline-JS modal (replaced with declarative Alpine markup)
TypeScript:
- web/src/bookPicker.ts: New module with Alpine.store and Alpine.data definitions
- web/src/main.ts: Import bookPicker module
- web/src/collections.ts: Remove old modal functions (replaced by Alpine)
This implements the Book Picker Modal feature from the collections system,
following SSR-first Alpine.js patterns with HTMX for dynamic updates.
Fixes "Add Books" button being disabled - modal now fully functional.
Change from global to page-specific JavaScript loading:
Remove:
- import "./bookshelf" (loaded globally on every page)
Add:
- import "./bookPicker" (needed globally for collections)
This change supports page-specific script loading strategy:
- Bookshelf: Loaded via <script> tag in bookshelf.templ only
- BookPicker: Loaded globally for collections page usage
Reduces JavaScript bundle size for pages that don't need bookshelf.
Matches SSR-first principle of progressive enhancement.
Add new bookPicker.ts module for multi-select book picker modal:
Alpine.store for global state:
- isOpen: Modal visibility state
- selectedBooks: Set<string> for persistent selection across HTMX swaps
- Methods: open, close, toggleBook, isSelected, loadBooks, clearFilters, submit
Key features:
- Selection persists across filter changes (Alpine.store)
- Multi-select with checkbox state management
- Adds books to collection via POST /api/collections/:id/books
- Trigger collection page reload after successful add
- Clear filters resets form fields (preserves selections)
- Uses HTMX for dynamic book grid updates
Critical SSR-first implementation:
- Alpine.store ensures state survives HTMX DOM swaps
- Checkboxes re-rendered by HTMX maintain state via store
- Selection persists across pagination and filter changes
- No DOM state, all state in Alpine reactive store
Replaces non-functional add books button in collections.
Complete rewrite following PROJECT_GUIDELINES.md procedural style:
Remove anti-patterns:
- Remove class-based OOP approach
- Remove manual DOM manipulation (classList.add/remove)
- Remove client-side data fetching in x-init
- Remove getEventListeners and manual event delegation
Add SSR-first patterns:
- Alpine.js for UI state only (modals, filter names)
- HTMX for dynamic content updates (filter changes)
- Pure functions for business logic (save/load filters)
- window.htmx.trigger() for programmatic HTMX triggers
- Server-side rendering for initial data load
Key features:
- saveFilter(): Save custom filter configurations
- loadSavedFilters(): Load user's saved filters
- initBookshelf(): Setup only (no data fetch)
- clearFilters(): Reset all filter fields
- showSaveFilterModal(): Open save filter modal
All Alpine state is local component data, not global store.
Follows ALPINE_COMPLETION_GUIDE.md principles strictly.
Add htmx to Window interface in alpine.ts to support:
- TypeScript type checking for htmx.trigger() calls
- Shared type declaration across bookshelf.ts and bookPicker.ts
- No imports needed - globally available via window.htmx
Declaration:
- trigger(element: HTMLElement | string, event: string): void
Used by bookshelf and bookPicker modules for HTMX programmatic triggers.
- Add book picker modal with Alpine.js state management for selecting books
- Add toggleBookPickerBook, isBookPickerBookSelected, getBookPickerSelectedCount methods
- Add clearBookPickerFilters function to reset filter form
- Fix icon picker: add showAllIcons function to reset icon search
- Fix setupHTMXModalInit to properly initialize Alpine tree after HTMX swap
- Update collections template with book picker modal structure
- Add bookshelf route with library selection from query param or first available
- Add filter bar UI with library selector, search, and filter controls
- Integrate HTMX for dynamic filtering (hx-get to /api/media-items/filtered)
- Add Alpine.js component for filter state management
- Add filter save/load functionality via /api/bookshelf/filters endpoint
- Update bookshelf.ts to use Alpine.js for reactive state instead of DOM manipulation
Replace direct DOM manipulation with Alpine.js reactive state variables:
- Add isLoading and hasBooks state to bookshelf component
- Convert loadBookshelf() to update isLoading state instead of toggling DOM visibility
- Convert renderBookshelf() to use reactive state for empty state handling
- Remove redundant getElementById() calls for loading/empty-state elements
This change improves maintainability by:
- Centralizing UI state in the Alpine component
- Eliminating direct DOM manipulation scattered across functions
- Making the component's state more explicit and trackable
- Following Alpine.js reactive programming patterns
The UI will now respond to state changes automatically rather than requiring
manual DOM updates throughout the lifecycle methods.
Fix TypeScript issues in device-management.ts and unlinked_books.ts:
1. device-management.ts:
- Move 'deviceType' variable declaration to function scope in showDeviceSettings()
- Previously declared inside a Promise chain, creating potential scope issues
- Now properly declared at function level before async operations
2. unlinked_books.ts:
- Remove unused 'result' parameter from .then() handlers
- Fixes autoLinkBook() and confirmManualLink() functions
- Handlers don't use the API response result, only need success/failure
These changes improve code clarity and resolve potential runtime issues
with variable accessibility in async callback chains.
Technical details:
- deviceType: moved from Promise .then() block to function scope
- Unused parameters: removed to prevent linting warnings and improve clarity
Since main.js has 'defer', the script executes after DOM is parsed.
The DOMContentLoaded check was unnecessary - the else branch always
executes. Simplified to just run immediately.
- header.ts now imports and re-exports functions from search.ts
and theme.ts for use in the header template
- Functions available via x-data=header:
- initializeSearch
- initializeTheme
- changeTheme
- changeWoodPaneling
- loadWoodPaneling
- updateWoodPanelingIndicators
- header.templ x-init calls these functions directly
- Enables proper SSR-first pattern with x-init for setup only
Since main.js has 'defer' attribute, the DOM is guaranteed to be
ready when modules execute. These wrappers are unnecessary.
dashboard.ts:
- Removed DOMContentLoaded wrapper, code runs directly
- Event delegation setup runs immediately
custom-section-builder.ts:
- Removed DOMContentLoaded wrapper
- initCustomSectionBuilder() called directly
toast.ts:
- Removed DOMContentLoaded wrapper
- initializeToastSystem() called directly at top level
- Removed dead Alpine.data registration (unused)
search.ts:
- Removed DOMContentLoaded wrapper
- initializeSearch exported for use in header
theme.ts:
- Removed DOMContentLoaded wrapper
- Functions now exported for use in header Alpine component
collections.templ:
- Removed ~75 lines of inline WebSocket JS
- Added initializeCollectionWebSocket using websocket.ts utility
- Updated template to use x-init for WebSocket init
admin.templ:
- Removed ~55 lines of inline WebSocket JS
- Added initializeScanWebSocket using websocket.ts utility
- Updated template to use x-init for WebSocket init
Both now use the shared websocket.ts createWebSocket function
- Removed ~400 lines of inline JavaScript from docs.templ
- Moved toggleSection function to docs.ts (now uses Alpine )
- Added highlightCurrentPage function to docs.ts
- Added initializeCodeCopyButtons function to docs.ts
- Updated template to use x-init for initialization
- Functions exported for use in Alpine.data
- Remove DOMContentLoaded event listeners from analytics.ts and docs.ts
- Rely on x-init attribute in templates for page initialization
- Clean up unused exports from collections.ts Alpine data
- Add x-init calls to admin_library, analytics, and docs templates
- Normalize quote style in collections WebSocket script (single to double)
- Disable Add Books button in collection detail (pending implementation)
CRITICAL FIX: initializeLibraryAdmin() was calling reloadLibraries()
which fetched data from the API and replaced the SSR-rendered library
list on page load, defeating the purpose of server-side rendering.
Changes in web/src/library.ts:
- Remove DOMContentLoaded listener (now uses Alpine x-init in template)
- Remove void reloadLibraries() call from initializeLibraryAdmin()
- Add comment explaining SSR provides initial data
- Add initializeLibraryAdmin to export statement
- Add initializeLibraryAdmin to Alpine.data() registration
- Keep reloadLibraries() as standalone function for use after CRUD ops
Rationale:
- SSR provides fast initial page load with library list
- x-init should ONLY setup event listeners, not fetch data
- reloadLibraries() is called after create/delete/update operations
- Follows SSR-first architecture: different pages have different
SSR/JS ratios (analytics is 80% JS, most pages are 80% SSR)
Documentation:
- Update COLLECTIONS_CLEANUP_GUIDE.md with SSR-first strategy
- Document page-by-page review status (dashboard ✓, collections 🔄)
- Fix template references (library.templ → admin_library.templ)
- Explain why analytics fetches data (intentional for dynamic page)
This ensures the admin library page maintains SSR benefits while
still providing interactive features via Alpine.js.
- Remove document.addEventListener("DOMContentLoaded") wrapper for loadWatchStatus()
- Simplify initialization - loadWatchStatus() is now called via Alpine.js x-init
- Reduces 4 lines, keeps same functionality
The loadWatchStatus() function is now triggered by template's x-init directive
instead of a global DOMContentLoaded listener, ensuring it only runs on the
admin page where it's actually needed.
- Remove DOMContentLoaded listeners for setupHTMXAuth, initColorSelection, and setupHTMXModalInit
- Delete dead Alpine.data exports: addbooksToAdd, removebooksToAdd, toggleBookForRemoval,
toggleBookSelection, initCollectionDetail, initIconSelection, initColorSelection
- Add missing setupHTMXAuth to export statement (it was called but not exported)
- Remove 14 lines of auto-initialization code that's no longer needed
This fixes "X is not defined" console errors for functions that were deleted
in commit 93710a1 but were still in Alpine.data export. The collections.templ template
was also updated to remove calls to these deleted functions.
These changes align with the SSR architecture where most collection functionality
is server-rendered and client-side JavaScript is used sparingly.
- Create createWebSocket() helper for WebSocket connections with authentication
- Support automatic reconnection with configurable delay
- Include error handling and logging
- Export disconnectWebSocket() for cleanup
This helper provides a centralized way to create WebSocket connections
with JWT token authentication from localStorage. Although not currently
used in the application (we opted for server-side token injection in templates),
it provides a reusable utility for future WebSocket integrations.
Features:
- Automatic token retrieval from localStorage
- Configurable reconnection behavior (enabled by default)
- Error handling with try-catch on all callbacks
- Connection cleanup and management
- Type-safe configuration interface
Available for future use in client-side WebSocket scenarios or as a reference
implementation.
- Regenerate Go template files with updated FileName paths for error reporting
- Apply Prettier formatting to api-explorer-docs.ts for consistency
- Format long function signatures across multiple lines for readability
- Format long conditional chains for better code clarity
Changes are purely formatting and do not affect functionality:
- api-explorer-docs.ts: Format initAPIExplorerDoc, tryDocEndpoint, and other functions
- Generated _templ.go files: Update FileName paths from relative to absolute (e.g., "admin.templ" → "templates/admin.templ")
This ensures consistent code style across the codebase and improves
error reporting by providing full file paths in template error messages.
- Add defer attribute to <script src="/static/main.js"> in 21 template files
- Improves page load performance by allowing HTML parsing to continue without blocking
- Maintains script execution order while enabling parallel resource loading
- Remove duplicate defer attribute from header.templ line 264
- Add websocket.ts import to main.ts for module registration
This optimization reduces page render blocking and improves perceived load times
by allowing the browser to continue parsing HTML while the main.js bundle loads.
The defer attribute ensures scripts execute in order after HTML parsing completes.
Affected templates include:
- Admin pages: admin, admin_library, admin_settings, admin_users
- Content pages: analytics, bookshelf, collections, conflicts, custom_section
- User pages: dashboard, devices, docs, index, login, profile, progress, queue, register
- System pages: header, unlinked_books