Commit Graph
135 Commits
Author SHA1 Message Date
john-okeefe 96d90db036 feat(reader): add core infrastructure for feature-based architecture
- Add reader-context.ts: defines ReaderContext interface and factory
- Add reader-events.ts: event bus for feature communication
- Add reader-state.ts: centralized state management
- Add reader-navigation.ts: unified navigation and rendering API
- Add reader-services.ts: shared services (progress, chapters)
- Establishes foundation for Feature Registration Pattern
2026-04-04 13:37:47 -04:00
john-okeefe 0c199963b9 Add universal reader features: gestures, keyboard, navigator, offline
- gestures.ts: Touch gesture controls (swipe, pinch, tap, double-tap)
- keyboard-shortcuts.ts: Keyboard navigation (arrows, space, page up/down, etc)
- navigator-panel.ts: Affinity-style minimap for page navigation
- offline-manager.ts: Service worker registration and online status
- reading-speed-tracker.ts: Track and sync reading speed to database
- Add web manifest.json for PWA support
2026-04-04 01:01:08 -04:00
john-okeefe 2046960563 Add ebook reader features: view modes, dictionary, copy handler
- view-modes.ts: Paginated, scrolled, single-column, double-column modes
- dictionary-popup.ts: Dictionary lookup with popup definitions
- copy-handler.ts: Copy text with automatic citation formatting
- Add en-US.json dictionary for offline word lookups
2026-04-04 01:01:01 -04:00
john-okeefe c94679ec1e Add comprehensive PDF reader enhancements
- pdf-page-sizes.ts: Dynamic page sizing and layout detection
- pdf-rotation.ts: Page rotation with orientation detection
- pdf-minimap.ts: Thumbnail minimap for navigation
- pdf-dual-page.ts: Dual page spread support
- pdf-links.ts: Clickable link handling
- pdf-clipbooard.ts: Copy to clipboard functionality
- pdf-bookmarks.ts: Bookmark management
- pdf-outline.ts: Document outline/TOC integration
- pdf-text-selection.ts: Text selection and highlighting
- page-cache.ts: PDF page caching system
- pdf-search.ts: Full-text search within PDF
- pdf-navigation.ts: Navigation controls and history
- annotation-layer.ts: PDF annotation rendering
- text-layer-renderer.ts: Text layer overlay rendering
- pdfjs-wrapper.ts: PDF.js wrapper with utilities
2026-04-04 01:00:49 -04:00
john-okeefe d60469a757 Add manga reading support with RTL and vertical scroll modes
- 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
2026-04-04 01:00:45 -04:00
john-okeefe c36d8e6b57 Add comic panel detection system with multi-tier fallback
- 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
2026-04-04 01:00:43 -04:00
john-okeefe 2eddc1b92b Add font loader and update reader template for font loading
- Add font-loader.ts for managing 8 reading fonts with preload optimization
- Include fonts: Literata, Crimson Text, Source Serif 4, EB Garamond,
  Libertinus Serif, Noto Serif, Charis SIL, IBM Plex Serif
- Update reader.templ to include reader-fonts.css stylesheet
2026-04-04 01:00:23 -04:00
john-okeefe d52937e578 Update font files with simplified naming and add reader font CSS
- 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
2026-04-04 01:00:13 -04:00
john-okeefe a387efad22 Add reading fonts with variable font support for premium typography
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.
2026-04-03 22:29:54 -04:00
john-okeefe d8bb5ff68a Implement client-side ebook parsers for EPUB, FB2, TXT, and HTML formats
- 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.
2026-04-03 22:29:33 -04:00
john-okeefe 1e47b0e459 Implement ebook reader with HTML rendering, typography engine, and search
- 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.
2026-04-03 22:29:25 -04:00
john-okeefe d3d84a8318 Implement core reader TypeScript modules for shell and UI management
- 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.
2026-04-03 22:29:20 -04:00
john-okeefe 0191d36dc9 Add TypeScript type definitions for reader feature
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).
2026-04-03 22:29:11 -04:00
john-okeefe 55ae91a147 api: add comic metadata fields to API responses
Update API layer to expose all 14 new comic metadata fields in media item
responses for both list and detail endpoints.

Handler Changes (internal/handlers/media.go):
- Added encoding/json import for JSON unmarshaling
- Updated ListMediaItems() to include 15 new fields in JSON response
- Updated GetMediaItem() to include 15 new fields in JSON response
- Added numericToFloat() helper: Convert pgtype.Numeric to float64
- Added jsonBytesToMap() helper: Convert JSONB []byte to map[string]interface{}
- Field name mapping: WebUrl (not WebURL), proper pgtype handling

TypeScript Types (web/src/types/api.d.ts):
- Updated MediaItemSummary interface with 15 new optional fields
- manga_type: 'unknown' | 'no' | 'yes' | 'yes_and_right_to_left'
- reading_direction: 'auto' | 'ltr' | 'rtl' | 'vertical'
- Universal fields: series_count, volume, imprint, age_rating, web_url, metadata_notes, community_rating
- Comic-specific: story_arc, is_black_and_white, alternate_info (nested type), scan_information, summary
- Proper TypeScript typing with string literals for type safety

API Response Fields Added:
Reading Direction:
- manga_type: Raw Manga field from ComicInfo.xml
- reading_direction: Computed direction (auto/ltr/rtl/vertical)

Universal Metadata (all formats):
- series_count, volume, imprint, age_rating, web_url, metadata_notes, community_rating

Comic-Specific:
- story_arc, is_black_and_white, alternate_info (JSONB object), scan_information, summary

Part of Phase 5: API Layer Updates
Implementation: IMPLEMENTATION_PLAN_MERGE_METADATA_READING_DIRECTION.md
2026-03-29 19:12:05 -04:00
john-okeefe a157c546fd feat: add book detail page links from browse pages
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.
2026-03-28 23:54:42 -04:00
john-okeefe eb349cbc95 feat: add book detail page with comprehensive metadata display
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)
2026-03-28 23:54:39 -04:00
john-okeefe 75df623982 Fix collections page Alpine errors and modal container issues
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
2026-03-28 21:20:58 -04:00
john-okeefe 9dccdfbde0 Fix load filter dropdown positioning on bookshelf page
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.
2026-03-28 20:35:40 -04:00
john-okeefe d9bb0834cd feat: add theme-aware tristate button styles with dynamic state rendering
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.
2026-03-28 00:46:18 -04:00
john-okeefe 486214d8a5 fix: refactor filter loading and clearing to prevent stale field data
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.
2026-03-28 00:46:14 -04:00
john-okeefe 01b1f0de79 fix: improve Clear Filters button functionality
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.
2026-03-27 18:08:09 -04:00
john-okeefe 4ea110cfeb refactor: replace genre with tags in frontend TypeScript
- 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
2026-03-25 20:38:21 -04:00
john-okeefe d3783eca8b feat: add autocomplete dropdown support for filter fields
- 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.).
2026-03-23 22:38:03 -04:00
john-okeefe 0cfd0bad52 feat: implement saved filter loading via API endpoint
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.
2026-03-21 23:04:05 -04:00
john-okeefe 63816fe6cd feat: implement SSR-first bookshelf page with saved filters and book grid
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.
2026-03-21 21:54:06 -04:00
john-okeefe 535097cefb style(web): fix async/await formatting in library.ts
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
2026-03-21 01:24:34 -04:00
john-okeefe ea2463fe44 fix(frontend): remove jarring forced reload on dashboard navigation
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
2026-03-21 01:24:21 -04:00
john-okeefe d1625fe231 feat(frontend): update bookshelf to use saved filters API
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
2026-03-21 00:16:14 -04:00
john-okeefe 86444ec7ae refactor: remove duplicate HTMX type declaration
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.
2026-03-20 22:58:21 -04:00
john-okeefe b77da3a289 refactor: migrate dashboard to SSR-first Alpine.js pattern
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)
2026-03-20 22:58:18 -04:00
john-okeefe e74eeb5c5b feat: implement collections book picker with Alpine.store
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.
2026-03-20 22:58:09 -04:00
john-okeefe 3c85a59c9e chore: rebuild Tailwind CSS with latest changes
Regenerate style.css with tailwindcss build process.
Includes updated utility classes for bookshelf and collections UI.
2026-03-20 11:51:11 -04:00
john-okeefe 17fd86aad3 refactor: update main.ts imports for page-specific bookshelf loading
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.
2026-03-20 11:51:11 -04:00
john-okeefe af1e61a0d7 feat: create book picker module for collections
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.
2026-03-20 11:50:33 -04:00
john-okeefe 3cf5d764a2 refactor: rewrite bookshelf TypeScript to SSR-first architecture
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.
2026-03-20 11:50:32 -04:00
john-okeefe 026eb4c086 feat: add global htmx type declaration for TypeScript
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.
2026-03-20 11:47:13 -04:00
john-okeefe f2cbb5a433 chore: add HTMX TypeScript types and book picker implementation plan
- Add htmx.d.ts with TypeScript type definitions for HTMX global
- Add BOOK_PICKER_IMPL.md with implementation plan for book picker modal
2026-03-16 16:24:26 -04:00
john-okeefe 6d92dff5e3 feat(collections): add book picker modal and fix icon picker
- 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
2026-03-16 16:24:10 -04:00
john-okeefe 5782a4e314 feat(bookshelf): add filter bar with HTMX integration and filter persistence
- 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
2026-03-16 16:24:03 -04:00
john-okeefe a9bbd1ee2e refactor(bookshelf): migrate from DOM manipulation to Alpine.js reactive state
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.
2026-03-15 21:02:58 -04:00
john-okeefe 855cbd1b74 fix(ts): resolve variable scoping and unused parameters in device management
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
2026-03-13 22:25:31 -04:00
john-okeefe 557ac77458 refactor(woodPanelingInit): simplify by removing DOMContentLoaded check
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.
2026-03-13 12:51:26 -04:00
john-okeefe 3e292f18c8 refactor(header): integrate search and theme functions via Alpine
- 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
2026-03-13 12:51:21 -04:00
john-okeefe 2075077bb7 refactor: remove unnecessary DOMContentLoaded wrappers
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
2026-03-13 12:51:16 -04:00
john-okeefe 41e7445524 refactor: remove inline WebSocket code from templates
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
2026-03-13 12:51:11 -04:00
john-okeefe 7068fabbee refactor(docs): remove inline JS from docs template
- 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
2026-03-13 12:50:51 -04:00
john-okeefe 06461a3202 refactor(alpine): migrate remaining pages to x-init declarative initialization
- 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)
2026-03-12 18:13:12 -04:00
john-okeefe 1b9bc64b28 refactor(library): fix SSR bug by removing data fetch from init function
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.
2026-03-12 17:58:36 -04:00
john-okeefe b06ffa2329 refactor(admin): remove DOMContentLoaded listener for watch status initialization
- 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.
2026-03-12 17:21:38 -04:00
john-okeefe 4d186f76dc refactor(collections): remove dead Alpine.js exports and DOMContentLoaded listeners
- 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.
2026-03-12 17:21:34 -04:00