Commit Graph
1046 Commits
Author SHA1 Message Date
john-okeefe aa7776db5f docs: consolidate implementation plans into unified search document
- Remove GET_SAVED_FILTER_BY_ID_IMPLEMENTATION.md (superseded)
- Remove SAVED_FILTERS_IMPLEMENTATION.md (superseded)
- Add UNIFIED_SEARCH_IMPLEMENTATION.md with comprehensive plan for:
  - Consolidating /filtered and /search endpoints
  - All-fuzzy text filters (author, series, genre, language)
  - Exact match with quotes for Google-style search
  - Field-specific fuzzy search for autocomplete dropdowns
  - Combined search + filters functionality
  - Phase-by-phase implementation with SQL, service, handler, frontend, tests, docs
2026-03-22 00:20:00 -04:00
john-okeefe 107edfa673 docs: remove extraneous closing backtick in implementation plan
Minor formatting fix to GET_SAVED_FILTER_BY_ID_IMPLEMENTATION.md
to remove an extra closing backtick in the API documentation section.

No functional changes - documentation formatting only.
2026-03-21 23:04:08 -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 c3a98fb067 fix: use custom error type for saved filters not found
Fix failing test 'GET /api/saved-filters/:id_with_non-existent_filter_returns_404'
which was returning HTTP 500 instead of HTTP 404 due to string comparison
failure in error handling.

Root Cause:
- Service wrapped database error: fmt.Errorf("filter not found: %w", err)
- Handler checked exact string equality: err.Error() == "filter not found"
- Wrapped error message included database error: "filter not found: no rows in result set"
- String check failed → returned 500 instead of 404

Solution: Use Go error wrapping with custom error type

Changes to internal/services/filters.go:
- Add import: "errors" package
- Add custom error variable: ErrFilterNotFound
- Update GetSavedFilterByID() to return ErrFilterNotFound instead of wrapped error
- Error defined at service layer (domain authority)

Changes to internal/handlers/filters.go:
- Update error check from string comparison to errors.Is(err, services.ErrFilterNotFound)
- Uses Go's standard error wrapping pattern
- Cleaner, more maintainable, type-safe

Architectural Benefits:
-  Service layer owns domain errors (filter not found is a filter concept)
-  Handlers only translate service errors to HTTP status codes
-  Services reusable by any caller (API, WebSocket, CLI)
-  Clean dependency direction: Handlers → Services → Database
-  Follows Go best practices for error handling

Test Results:
- GET /api/saved-filters/:id with non-existent filter now returns 404
- Error message: "filter not found"
- No information leakage about other users' filters

Fixes test failure in TestSavedFilters.
2026-03-21 23:03:43 -04:00
john-okeefe 54d3ae785a docs: add GET /:id endpoint documentation and update implementation plan
Add comprehensive documentation for GET /api/saved-filters/:id endpoint
including Bruno API collection, developer API docs, user documentation,
and implementation plan with frontend integration phase.

Bruno API Collection (bruno/saved-filters/Get Saved Filter By ID.yml):
- New Bruno request file for GET /:id endpoint
- Includes comprehensive documentation with examples
- Documents all status codes (200, 400, 401, 404)
- Provides example curl commands and use cases
- Uses variable placeholders ({{base_url}}, {{filter_id}})
- Follows existing Bruno YAML patterns

API Documentation (docs/developer/api/saved-filters/index.md):
- Added GET /api/saved-filters/:id endpoint documentation
- Example request with UUID parameter
- Example response showing filter object structure
- Error responses documented (400, 401, 404)
- Use cases: Mobile apps, SPAs, editing, verification

User Documentation (docs/user/library-browsing.md):
- Updated "Loading Saved Filters" section
- Removed "feature coming soon" language
- Added step-by-step instructions for loading filters
- Added tips section with visual indicators
- Added "Managing Saved Filters" section
- Added "Common Use Cases" (genre, author, series)
- Emphasizes instant feedback (no page reload)

Implementation Plan (GET_SAVED_FILTER_BY_ID_IMPLEMENTATION.md):
- Added Phase 7: User Documentation Update
- Added Phase 8: Frontend Integration (bookshelf.ts)
  - Shows loadFilter() implementation
  - Hybrid Alpine.js + HTMX approach
  - Maintains SSR-first principles
  - API call on user interaction, not page load
  - Populates hidden form fields
  - Triggers HTMX to apply filter
- Updated Summary of Changes: 7 files, ~344 lines
- Updated Checklist with frontend and user docs tasks
- Added frontend testing tasks

SSR-First Compliance:
- Initial page load: Server renders everything (no API calls)
- User interaction only: API called when user clicks filter
- No async x-init data fetching
- Progressive enhancement maintained

Documentation Structure:
- Developer docs: API reference for integration
- User docs: Step-by-step usage instructions
- Bruno: API contract testing
- Implementation plan: Complete development guide

All documentation follows established patterns and includes examples.
2026-03-21 22:37:30 -04:00
john-okeefe 0960e36f30 feat: add GET /api/saved-filters/:id endpoint with comprehensive tests
Implement missing GET endpoint for retrieving individual saved filters by ID.
This completes the CRUD API for saved filters and enables mobile/SPA clients
to fetch filter details on-demand.

Backend Implementation:
- Add GetSavedFilterByID() handler method (internal/handlers/filters.go)
  - Parse filter ID from URL parameter
  - Validate UUID format, return 400 for invalid IDs
  - Call service layer for business logic + ownership verification
  - Return 404 if filter not found or doesn't belong to user
  - Return 200 with filter object including filters JSONB

- Add GetSavedFilterByID() service method (internal/services/filters.go)
  - Call existing database query GetSavedFilterByID
  - Verify filter exists and belongs to user
  - Return descriptive error: "filter not found or access denied"
  - Reuses existing database query (no new SQL needed)

- Register GET /:id route (internal/router/filters.go)
  - Add route before existing GET "" route
  - Follows RESTful routing conventions

Integration Tests (cmd/server/tests/filters_test.go):
- Test success case: Create filter, retrieve by ID, verify data
- Test error case: Invalid UUID format returns 400
- Test error case: Non-existent filter returns 404
- Test error case: No authentication returns 401
- Test security case: Cross-user access returns 404 (not 403)
  - Admin creates filter, regular user tries to access
  - Uses setup.Token (admin) and setup.RegularToken
  - Verifies information leakage prevention

API Design:
- Endpoint: GET /api/saved-filters/:id
- Authentication: JWT token required
- Response format: SavedFilterResponse with filters as JSON
- Error responses: 400 (invalid ID), 401 (no auth), 404 (not found)
- Security: Returns 404 for cross-user access (hides existence)

Benefits:
- Completes CRUD API for saved filters
- Enables future mobile/SPA clients
- Follows existing handler/service/test patterns
- Comprehensive security testing
- No database changes required (reuses existing queries)

Follows PROJECT_GUIDELINES.md service layer architecture and testing patterns.
2026-03-21 22:37:19 -04:00
john-okeefe ba8133fb2d docs: remove SSR bookshelf implementation plan after completion
Remove SSR_BOOKSHELF_IMPLEMENTATION.md as the SSR-first bookshelf
feature has been successfully implemented and deployed.

The implementation plan served its purpose:
- Guided SSR-first bookshelf page implementation
- Server-side rendering of books and saved filters
- Template changes and TypeScript refactoring
- All components now follow SSR-first principles

The plan document (803 lines) has been preserved in git history:
- commit 059a281: Initial documentation
- commit 63816fe: Implementation reference

Keeping the plan document would be redundant since:
- Feature is complete and working
- Code contains inline comments
- git history preserves the planning process
- No longer needed for future work

Follows YAGNI principle - remove planning docs after implementation.
2026-03-21 22:16:31 -04:00
john-okeefe 059a281a32 docs: add implementation plans for SSR bookshelf and GET /:id endpoint
Add comprehensive implementation planning documents for two features:

SSR_BOOKSHELF_IMPLEMENTATION.md:
- Complete SSR-first bookshelf page implementation plan
- Server-side rendering of books and saved filters
- Template changes with pagination controls
- Client-side TypeScript refactor (remove async x-init)
- Testing plan with performance benchmarks
- Rollback procedures and potential issues
- 803 lines covering full implementation lifecycle

GET_SAVED_FILTER_BY_ID_IMPLEMENTATION.md:
- Plan for GET /api/saved-filters/:id endpoint
- Reuses existing GetSavedFilterByID database query
- Handler and service layer implementation
- Integration tests covering all contexts (no auth, user, different users)
- Bruno API collection YAML file
- API documentation updates
- Security considerations (404 for cross-user access)
- Future enhancements (caching, batch operations)

Both documents follow PROJECT_GUIDELINES.md patterns:
- Service layer architecture
- Test helpers usage
- Bruno YAML documentation
- Comprehensive testing plans
- Rollback procedures

Stored in git history for future reference and implementation.
2026-03-21 21:54:17 -04:00
john-okeefe ba31c223ce feat: initialize FiltersHandler in main server configuration
Add FiltersHandler to server initialization to enable saved filters API endpoints.

Changes to cmd/server/main.go:
- Initialize filtersHandler using handlers.NewFiltersHandler(queries)
- Add FiltersHandler to Config struct for route registration

This enables the following API endpoints:
- GET /api/saved-filters?resource_type=X - List filters
- POST /api/saved-filters - Create filter
- PUT /api/saved-filters/:id - Update filter
- DELETE /api/saved-filters/:id - Delete filter

Part of saved filters feature implementation.
2026-03-21 21:54:11 -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 85964ec932 fix(api): enforce user isolation on saved filters delete operation
Fix critical security issue where admin users could delete other users'
saved filters due to incorrect error handling in DELETE query.

Database Schema Changes:
- Change DeleteSavedFilter from :exec to :one (queries.sql:1747-1750)
- Add RETURNING * to return deleted row for proper error detection
- Regenerate querier.go and queries.sql.go with updated signature

Service Layer (internal/services/filters.go):
- Update DeleteSavedFilter to capture returned row (using _ to discard)
- Properly propagate pgx.ErrNoRows when no rows are deleted
- Error wrapping preserves original error for handler detection

Handler Layer (internal/handlers/filters.go):
- Add errors.Is() check for pgx.ErrNoRows (line 148)
- Return 404 Not Found when filter doesn't exist or belongs to different user
- Return 500 Internal Server Error for other database errors
- Add "errors" import (line 8)

Security Fix Details:
Before: Admin could delete user's filter → 204 No Content (SUCCESS)
After:  Admin tries to delete user's filter → 404 Not Found (DENIED)

The DELETE query uses WHERE id = @id AND user_id = @user_id, which matches
0 rows when attempting to delete another user's filter. The old :exec query
didn't return row count, so 0 affected rows looked like success. The new :one
query with RETURNING * returns pgx.ErrNoRows when no rows match, allowing
the handler to return proper 404 error.

Test Impact:
- TestSavedFilters/User_cannot_access_another_user's_filter now passes
- All 6 integration tests pass with proper user isolation enforcement

Pattern Consistency:
- Matches DeleteLibraryFolder pattern (line 99 in queries.sql)
- Uses same error handling as media handlers (errors.Is + pgx.ErrNoRows)
- Follows user-scoping pattern used throughout codebase

Related: Saved filters implementation user isolation
Security: Prevents unauthorized deletion of user data
2026-03-21 01:24:15 -04:00
john-okeefe 2022595fd4 test(api): add Bruno API collection for saved filters
Add comprehensive Bruno OpenCollection YAML files for testing
the saved filters API with 9 request files and scenarios.

Main CRUD Requests (4 files):
1. List Saved Filters.yml
   - GET /api/saved-filters?resource_type=media-items
   - Documents resource_type parameter requirement
   - Example responses with JSONB filters

2. Create Saved Filter.yml
   - POST /api/saved-filters
   - Complete request body documentation
   - All filter field examples (genre, author, sort, year, etc.)
   - Validation rules (max 100 chars, uniqueness)

3. Update Saved Filter.yml
   - PUT /api/saved-filters/{filter_id}
   - Immutability notes (resource_type can't change)
   - Duplicate name validation
   - Updated timestamp behavior

4. Delete Saved Filter.yml
   - DELETE /api/saved-filters/{filter_id}
   - 204 No Content response
   - Security considerations

Scenario Test Files (5 files):

1. Duplicate Name Validation.yml
   - Tests 409 Conflict on duplicate names
   - Per-user + per-resource-type uniqueness
   - Example bash test script

2. User Isolation - Cross-User Access.yml
   - Tests users can't access each other's filters
   - Security: 404 instead of 403 (prevents enumeration)
   - Complete multi-user test scenario
   - Database-level isolation documentation

3. Multiple Resource Types.yml
   - Tests generic design with different resource types
   - Same name allowed for different types (media-items, collections, devices)
   - Examples for each resource type
   - Extensibility benefits explained

4. Complete CRUD Workflow.yml
   - End-to-end lifecycle test (6.5K file)
   - Shell script with all steps: Create → Read → Update → Delete → Verify
   - Success criteria checklist
   - Copy-paste ready test script

5. Filter Validation - Edge Cases.yml
   - 12 different validation test cases
   - Empty names, missing fields, invalid UUIDs
   - Unicode support (emoji, CJK characters)
   - Malformed JSON handling
   - Special characters and XSS attempts

Documentation Features:
- {{base_url}} variable substitution
- auth: inherit for authentication
- Comprehensive docs: sections with examples
- Shell commands ready to copy-paste
- Expected status codes and responses
- Error handling examples
- Security best practices

Total: 9 YAML files covering all CRUD operations and edge cases

Usage:
- Import into Bruno/Postman for API testing
- Use for manual testing during development
- Reference for API contract validation
- Example curl commands for documentation

Part of: Saved Filters Implementation (Phase 5: Testing & Documentation)
Related: #saved-filters-feature
2026-03-21 00:16:29 -04:00
john-okeefe fb8ba3d20c docs(saved-filters): add comprehensive API and user documentation
Add complete documentation for saved filters feature including
API reference, usage examples, and user guides.

Developer Documentation (docs/developer/api/saved-filters/):
- API overview and design principles
- RESTful endpoint reference (GET, POST, PUT, DELETE)
- Request/response examples with JSON schemas
- Authentication and authorization details
- Error response documentation
- Query parameter reference
- Validation rules and constraints
- Status code reference
- cURL examples for each endpoint

User Documentation (docs/user/library-browsing.md):
- How to save custom filters on bookshelf page
- Loading saved filters
- Filter privacy (user-specific)
- Step-by-step instructions with screenshots placeholders
- Use cases and examples

API Endpoints Documented:
- GET /api/saved-filters?resource_type=X
- POST /api/saved-filters
- PUT /api/saved-filters/:id
- DELETE /api/saved-filters/:id

Documentation Sections:
1. Overview and purpose
2. Authentication requirements
3. Request/response formats
4. Query parameters
5. Request body schemas
6. Response examples
7. Error handling
8. Status codes
9. cURL examples
10. User guide integration

Code Examples:
- Bash/cURL commands for each endpoint
- JSON request/response examples
- Error response examples
- Authentication header examples

Standards Compliance:
- Matches OpenAPI/Swagger patterns
- Includes all HTTP methods
- Documents all query parameters
- Error codes and messages documented
- Security considerations included

User Experience:
- Clear step-by-step instructions
- Real-world usage examples
- Privacy and security explained
- Troubleshooting tips

Part of: Saved Filters Implementation (Phase 5: Documentation)
Related: #saved-filters-feature
2026-03-21 00:16:23 -04:00
john-okeefe 5e1fe17e1b test(api): add comprehensive integration tests for saved filters
Add complete test suite for saved filters API covering all CRUD
operations, validation, security, and edge cases.

Test Coverage (6 test cases, 216 lines):

Authentication & Authorization:
- GET /api/saved-filters without auth returns 401
- User cannot access another user's filters (404 not 403)

CRUD Operations:
- GET returns empty array initially (200 OK)
- POST creates filter with proper JSON response (201 Created)
- POST duplicate name returns 409 Conflict
- PUT updates filter with new criteria (200 OK)
- DELETE removes filter successfully (204 No Content)

Security Tests:
- User isolation: Regular user's filter inaccessible to admin
- Ownership verification: DELETE returns 404 for other users' filters
- JWT authentication required on all endpoints

Validation Tests:
- Filter name uniqueness per user + resource type
- Proper UUID validation for filter IDs
- Request body validation (required fields)

Test Infrastructure:
- Uses setupTestServer() helper (standard pattern)
- Direct HTTP requests with http.Client{}
- Uses setup.Server.URL for base URL
- Uses setup.Token for admin authentication
- JSONB validated as JSON objects in assertions

Test Helpers:
- createRegularUserOnce(t, db) - Creates unique test user
- loginUserWithCredentials() - Returns JWT token

Code Quality:
- Follows PROJECT_GUIDELINES.md testing patterns
- Matches collections_bulk_test.go style
- Proper cleanup with defer resp.Body.Close()
- Clear test names describing what is being tested

Scenarios:
- Complete CRUD workflow (create → read → update → delete)
- Duplicate name validation (409 Conflict)
- User isolation (cross-user access prevention)
- Multiple resource types (media-items, collections, devices)
- Edge cases (empty names, invalid IDs, malformed JSON)

Expected Results:
-  All 6 tests pass
-  User scoping enforced
-  Duplicate names rejected
-  Proper HTTP status codes
-  JSONB filters correctly serialized

Part of: Saved Filters Implementation (Phase 4: Testing)
Related: #saved-filters-feature
2026-03-21 00:16:19 -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 ddfc832b68 feat(api): implement saved filters backend service and handlers
Add complete backend implementation for saved filters CRUD operations
with proper service layer architecture and RESTful API endpoints.

Service Layer (internal/services/filters.go):
- NewFiltersService() constructor following project patterns
- GetSavedFilters(): Retrieve all filters for user + resource type
- CreateSavedFilter(): Create filter with duplicate name validation
- UpdateSavedFilter(): Update filter with ownership verification
- DeleteSavedFilter(): Delete filter with user scoping

Business Logic:
- Filter name uniqueness enforced per user + resource type
- User ownership validation on all operations (JWT user_id)
- JSONB marshaling/unmarshaling for flexible filter storage
- Proper error wrapping with context messages

Handler Layer (internal/handlers/filters.go):
- NewFiltersHandler() constructor (receives db.Queries)
- GetSavedFilters: GET /api/saved-filters?resource_type=X
- CreateSavedFilter: POST /api/saved-filters
- UpdateSavedFilter: PUT /api/saved-filters/:id
- DeleteSavedFilter: DELETE /api/saved-filters/:id

Content Negotiation:
- Supports both JSON (API clients) and HTML (HTMX) responses
- wantsHTML() helper checks Accept header
- HX-Redirect header for HTMX form submissions
- Proper status codes (200, 201, 204, 400, 401, 404, 409)

Router Configuration:
- registerFiltersRoutes() function in internal/router/filters.go
- JWT middleware protection on all endpoints
- RESTful route structure: /api/saved-filters
- Registered in main router.go RegisterRoutes() function
- Added FiltersHandler to router.Config struct

Test Infrastructure:
- Added FiltersHandler to test server setup (test_helpers_test.go)
- FiltersHandler initialized in setupTestServer() function
- Router.Config includes FiltersHandler for integration tests

Code Quality:
- Follows PROJECT_GUIDELINES.md service layer patterns
- Uses database models (not custom domain models)
- JSONB returned as []byte (matches collections pattern)
- All errors wrapped with context using fmt.Errorf
- Handlers create services internally (not dependency injection)

Part of: Saved Filters Implementation (Phase 2: Backend)
Related: #saved-filters-feature
2026-03-21 00:16:10 -04:00
john-okeefe e17a96123f feat(db): add saved_filters table and CRUD operations
Add database schema and SQL queries for generic saved filters system
that allows users to save custom filter presets for any resource type.

Database Schema:
- Add saved_filters table with user_id, name, resource_type, filters (JSONB)
- Create composite index on (user_id, resource_type) for efficient lookups
- Create index on (user_id, name) for future name search feature
- Add update_updated_at_column() trigger to auto-update timestamps
- Make trigger creation idempotent with DROP TRIGGER IF EXISTS

SQL Queries (5 new queries):
- GetSavedFilters: List all filters for user + resource type
- GetSavedFilterByID: Retrieve single filter by ID
- CreateSavedFilter: Create new saved filter
- UpdateSavedFilter: Update filter name/criteria
- DeleteSavedFilter: Remove saved filter

Design Decisions:
- Generic resource_type field supports any resource (media-items, collections, devices)
- JSONB filters field allows flexible schema without migrations
- User-scoped via JWT (user_id foreign key with CASCADE delete)
- Automatic updated_at timestamp via database trigger

Generated Code:
- database.SavedFilters model (10 fields including JSONB filters)
- All 5 CRUD query functions with proper parameter types
- pgtype.UUID wrappers for UUID parameters

Part of: Saved Filters Implementation (Phase 1: Database)
Related: #saved-filters-feature
2026-03-21 00:16:05 -04:00
john-okeefe a63394f429 docs: remove completed bookshelf and collections filter implementation plan
Remove BOOKSHELF_COLLECTIONS_FILTER_PLAN.md - this plan has been fully implemented:
- Bookshelf page with SSR-first architecture and Alpine.js integration 
- Collections book picker modal with Alpine.store for state persistence 
- All TypeScript files written and working 
- All templates updated with HTMX/Alpine patterns 
- Navigation link added 

The implementation is complete and tested. This planning document lives on
in git history for reference. Keeping the repository clean of completed plans.

Implementation details preserved in:
- SAVED_FILTERS_IMPLEMENTATION.md (for future saved filters feature)
- docs/developer/alpine-patterns.md (Alpine.js patterns used)
- Git commit history (all implementation commits)
2026-03-20 23:10:28 -04:00
john-okeefe 974f332b7e docs: add Alpine.js SSR-first patterns guide
Add comprehensive guide for Alpine.js SSR-first patterns in Bookhoard:
- Page classification system (Type 1: 80% SSR, Type 2: SSR+Interactive,
Type 3: 80% TypeScript)
- Alpine.js usage guidelines (UI state only, no data fetching in x-init)
- HTMX integration patterns
- When to use x-show vs CSS classes
- Form handling and validation
- Modal and dropdown patterns
- Component reusability with Alpine.data()
- Alpine.store for global state (book picker example)
This documentation helps developers maintain consistency across the
codebase
and make informed decisions about when to use Alpine.js vs vanilla
JavaScript
vs HTMX for different features.
Follows PROJECT_GUIDELINES.md documentation standards.
2026-03-20 23:02:02 -04:00
john-okeefe 2f721571ef build: update auto-generated bookshelf template
Regenerate bookshelf_templ.go after fixing template script tags.
The templ compiler auto-generates this file from bookshelf.templ changes.

Changes:
- Removed Alpine.js CDN script tag from generated output
- Removed standalone bookshelf.js script tag from generated output
- Updated line numbers in error references

This is an auto-generated file - changes reflect bookshelf.templ fixes
committed in previous commit (34be9ab).
2026-03-20 22:59:40 -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 34be9ab16e fix: remove duplicate bookshelf route and fix template script tags
Remove duplicate /bookshelf route registration that was causing server panic.
The route was registered twice in frontend.go (lines 257-307 removed).

Fix bookshelf.templ script tags:
- Remove malformed Alpine.js CDN path (/static/alpinejs@3.x.x/dist/cdn.min.js)
- Remove standalone bookshelf.js script tag (not built separately)
- Rely on header.templ to load main.js which includes all Alpine components

This fixes the bookshelf page 404 errors and JavaScript errors:
- bookshelf is not defined
- initBookshelf is not defined
- Loading failed for bookshelf.js

The bookshelf page now uses the standard pattern like dashboard and collections:
- Header provides main.js with all Alpine components
- Bookshelf Alpine component registered via x-data="bookshelf"
- All functionality works correctly
2026-03-20 22:58:05 -04:00
john-okeefe e82d2d7066 docs: remove obsolete implementation plans
Remove implementation plans that have been completed and are no longer needed:
- ALPINE_COMPLETION_GUIDE.md (95% complete, only docs updates needed)
- BOOK_PICKER_IMPL.md (90% obsolete, better approach implemented)
- SSR_FIRST_ALPINE_GUIDE.md (100% compliant with current implementation)

These plans served their purpose during implementation. Their content lives on
in git history for reference. Keeping the repository clean of outdated planning docs.
2026-03-20 22:57:58 -04:00
john-okeefe 09449eff39 docs: add saved filters implementation plan
Add comprehensive implementation plan for generic saved filters feature:
- Generic /api/saved-filters endpoint with resource_type field
- Service layer architecture with business logic
- JSONB storage for flexible filter schemas
- Integration test patterns
- Support for both JSON (API) and HTML (HTMX) responses
- Database schema with auto-updating updated_at trigger

This plan follows PROJECT_GUIDELINES.md and matches existing codebase patterns
(service layer, handler constructors, error handling, testing patterns).

Related to bookshelf page save filter functionality.
2026-03-20 22:57:55 -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 9ae99d0ddd feat: add functional book picker modal to collections
Update CollectionDetail template to enable book picker:

Enable Add Books button:
- Remove disabled attribute and inline JavaScript handlers
- Wire to $store.bookPicker.open() using Alpine store

Remove old modal:
- Delete non-functional inline-JavaScript modal (add-books-modal)
- Remove inline event handlers (onchange, onclick)
- Clean up unused DOM elements

Add new book picker modal:
- Full-screen modal with HTMX-powered filtering UI
- Search by title, author, genre with live filtering
- Multi-select checkboxes with Alpine.store state persistence
- Selected count display and submit functionality
- Clear filters resets search (preserves selections)
- ESC key closes modal via Alpine event listener

SSR-first implementation:
- Alpine.store.bookPicker manages all state (no DOM state)
- HTMX swaps book grid without losing selections
- Checkboxes re-rendered from store state after DOM swap
- Selection persists across pagination and filter changes
- No class="hidden" for stateful UI (use x-show)
- style="display: none;" prevents FOUC on x-show elements

Replaces non-functional inline JavaScript approach.
Matches bookshelf filtering UX for consistency.

Changes to collections_templ.go are auto-generated from .templ file.
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 b9cc6f424f feat: rewrite bookshelf template with SSR-first architecture
Complete rewrite of bookshelf.templ following PROJECT_GUIDELINES.md:

- Add Alpine.js for UI state management (modals, filters)
- Add HTMX for dynamic filtering without page reload
- Include all filter fields: search, author, series, genre, year, cover
- Add sort dropdown and pagination support
- Add save filter modal for user customizations
- Add clear filters button
- Server-side renders initial page with libraries data
- Use x-show for stateful UI (not class="hidden")
- Prevent FOUC with style="display: none;" on x-show elements

Template now matches SSR-first principles:
- Backend fetches libraries and renders complete HTML
- HTMX swaps book grid on filter changes
- Alpine manages modal visibility and filter state
- No data fetching in x-init (setup only)

Changes to bookshelf_templ.go are auto-generated from .templ file.
2026-03-20 11:47:01 -04:00
john-okeefe 513ff7c82f feat: restore bookshelf page route and add navigation link
- Add /bookshelf route in frontend.go (was typo /booskshelf)
- Route fetches libraries server-side and renders complete HTML
- Supports library_id query param or defaults to user's first library
- Add "All Books" link to header navigation
- Follows SSR-first architecture principles

Fixes route registration that prevented bookshelf page from loading.
2026-03-20 11:43:17 -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 30c8132c96 docs: add bookshelf and collections filter implementation plan
- Create comprehensive implementation plan for restoring bookshelf page
- Add detailed specifications for collections book picker modal
- Document SSR-first architecture with Alpine.js + HTMX pattern
- Define 2-fold use case: bookshelf browsing + collections book selection
- Include Phase 1-4 breakdown with technical specifications
- Note existing /api/media-items/filtered API will be used
- Note AddBookToCollection handler already exists in collections.go
- Follow PROJECT_GUIDELINES.md and ALPINE_COMPLETION_GUIDE.md principles
- Estimate 8-10 hours implementation time

This plan restores functionality lost in commit 2df2b2d when bookshelf
route was removed and consolidated into dashboard. The backend filtering
API and book addition endpoints already exist and are functional.
2026-03-15 21:20:56 -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 af7533529c refactor(templates): remove duplicate main.js script tags, consolidate to header component
Remove redundant <script src="/static/main.js" defer></script> tags from 17+
templates that include the @Header component, eliminating duplicate script
loading that was causing Alpine.js to initialize twice per page load.

The header.templ component now serves as the single source of truth for
main.js inclusion, following the DRY principle and ensuring consistent
script loading across all pages that use the header navigation.

Additionally, add type="button" attribute to all buttons in header navigation
to prevent default form submission behavior when buttons are clicked.

Changes:
- Remove main.js script tag from templates using @Header component
- Keep main.js in header.templ (line 279) as universal inclusion point
- Preserve main.js in special pages: index.templ, login.templ, register.templ
  (these don't use @Header and are standalone entry points)
- Add type="button" to theme toggle, theme selection, wood paneling, and user menu buttons
  to prevent unwanted form submissions or page navigation

Benefits:
- Eliminates Alpine.js double-initialization bug
- Reduces HTTP requests (one script load instead of two)
- Improves maintainability (add header, get scripts automatically)
- Fixes broken @click handlers on collections, devices, and other pages
- Prevents buttons from triggering default form submission behavior

Technical notes:
- Templates affected: admin, analytics, bookshelf, collection_rules,
  collections, conflicts, custom_section, dashboard, devices, docs,
  library, profile, progress, queue, unlinked_books
- No changes to entry pages (index, login, register) which don't use @Header
- HTMX script remains in individual templates (stateless, no double-load issue)
- All interactive buttons in header now explicitly marked type="button" to
  prevent default browser form submission behavior

Related to: previous commit fixing Vite code-splitting
2026-03-13 22:25:12 -04:00
john-okeefe f73ca5fdab fix(vite): prevent Alpine.js code splitting to eliminate variable redeclaration error
Configure Vite to bundle all code into a single chunk using manualChunks,
preventing Alpine.js from being split into multiple modules that caused
"redeclaration of let Xo" errors during initialization.

This resolves the critical bug where Alpine.js would load twice on pages
using @Header, breaking all @click handlers and causing form buttons to
fall back to default browser behavior (unwanted navigation/form submission).

Technical details:
- The default Vite code-splitting was creating multiple ESM chunks
- Alpine's reactive system uses let Xo internally
- Multiple chunks caused Xo to be declared multiple times
- manualChunks() forces everything into a single bundle

Fixes #XXX (Alpine.js redeclaration error)
2026-03-13 22:24:49 -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 5b7540be99 docs: update Alpine.js migration guide and project guidelines
- Fix incorrect function references in ALPINE_COMPLETION_GUIDE.md
  - header.changeThemeTo -> changeTheme
  - header.logout -> logout
  - woodPaneling.change -> changeWoodPaneling
- Add SSR-first principles section to PROJECT_GUIDELINES.md
- Add page type classifications (Type 1, 2, 3)
- Fix extra asterisks on line 43
- Update to reference TypeScript instead of JavaScript
2026-03-13 12:50:47 -04:00