Skip to content

feat: Sortable columns on library page (#111)#146

Merged
deucebucket merged 4 commits intodevelopfrom
feature/issue-111-sortable-columns
Feb 11, 2026
Merged

feat: Sortable columns on library page (#111)#146
deucebucket merged 4 commits intodevelopfrom
feature/issue-111-sortable-columns

Conversation

@deucebucket
Copy link
Owner

Summary

  • Click Author, Title, or Status column headers to sort ascending
  • Click again for descending, third click clears sort
  • Sort state preserved across filter changes and pagination
  • Backend validates sort column names against a whitelist (no SQL injection)
  • Works across all filter views: verified, pending, queue, errors, search, media types, etc.
  • Orphan sorting handled in Python since orphans aren't from SQL

Test plan

  • Click Author header — items sort A-Z, arrow indicator shows
  • Click again — items sort Z-A, arrow flips
  • Click third time — sort clears, back to default order
  • Switch filter tabs — sort resets, no errors
  • Try Title and Status columns same way
  • Search with sort active — results are sorted
  • Pagination with sort — page 2+ maintains sort order

Add clickable Author, Title, and Status column headers that toggle
ascending/descending sort. Third click clears sort back to default.
Backend validates sort columns against a whitelist to prevent injection.
@bucket-agent
Copy link

bucket-agent bot commented Feb 11, 2026

🔍 Vibe Check Review

Context

PR #146 adds sortable columns (author, title, status) to the library page with proper whitelisting, three-state sorting (asc → desc → clear), and visual indicators.

Codebase Patterns I Verified

  • Request parameters: Consistent use of request.args.get() pattern (verified in lines 8945-8947, 8953, 8973)
  • Type hints: Project does NOT use type hints on internal functions (verified via grep - only test files use them)
  • SQL queries: Project uses parameterized queries with ? placeholders (verified across app.py)
  • f-strings: This is the FIRST use of f-strings in SQL in this codebase - BUT it's done correctly with whitelisting
  • Error handling: Project uses validation to prevent errors rather than try/except everywhere
  • Comments: Project references issue numbers in comments (seen throughout)

✅ Good

  • Security done RIGHT: Whitelisted column names and validated sort direction prevent SQL injection
  • Smart helper function: build_order_by() eliminates code duplication across 13 SQL queries
  • Great UX: Three-state sorting (asc → desc → clear) is more intuitive than two-state
  • Visual feedback: Hover effects, sort arrows, and active state clearly indicate sort status
  • Consistent: Follows existing patterns for parameter handling and commenting
  • Complete implementation: Handles all filter contexts (orphans, pending, queue, fixed, etc.)
  • Proper fallbacks: Returns to default sort order when sort is cleared
  • Orphan sorting: Special handling for in-memory orphan list (can't use SQL ORDER BY)

🚨 Issues Found

NONE - This is clean, secure code.

Severity File:Line Exact Code Quote Issue Fix
- - - - -

📋 Scope Verification

Issue Problem Addressed? Notes
#111 Add sortable columns Fully implemented: Author, Title, Status columns all sortable with visual indicators

Scope Status: SCOPE_OK

📝 Documentation Check

  • CHANGELOG.md: ❌ Missing - This is a feat: PR and should have a CHANGELOG entry
  • README.md: N/A - Feature is UI-only, no user-facing documentation needed beyond CHANGELOG

Recommendation: Add CHANGELOG entry before merge:

## [0.9.0-beta.XXX] - YYYY-MM-DD

### Added

- **Issue #111: Sortable columns** - Library table columns (Author, Title, Status) can now be sorted
  - Click column headers to toggle sort order: ascending → descending → default
  - Visual indicators show active sort column and direction
  - Sort state preserved during pagination and filter changes
  - Works across all filter contexts (pending, queue, fixed, verified, etc.)

🎯 Verdict

REQUEST_CHANGES

Required before merge:

  1. Add CHANGELOG.md entry (see recommendation above)

Why this is good code:

  • Security is handled correctly with whitelisting (not SQL injection despite f-strings)
  • Implementation is complete and consistent
  • UX is thoughtful (three-state sorting, visual feedback)
  • Code quality is high (helper function, good comments, no duplication)

The ONLY issue is missing documentation - the implementation itself is excellent. Fix the CHANGELOG and this is ready to ship.

@bucket-agent
Copy link

bucket-agent bot commented Feb 11, 2026

🔍 Vibe Check Review

Context

PR #146 adds sortable columns (Author, Title, Status) to the library page table with visual indicators and persistent state across pagination/filters.

Codebase Patterns I Verified

  • SQL queries: Project uses parameterized queries with ? placeholders (checked existing queries in app.py)
  • f-strings: Project uses f-strings extensively (534 occurrences in app.py)
  • Type hints: Not consistently used in this project
  • Error handling: Varies by context, not always present
  • Comments: Issue references common (e.g., # Issue #111:)

✅ Good

  • SQL Injection Protection: Whitelist validation on sort columns prevents SQL injection (BOOK_SORT_COLS, HISTORY_SORT_COLS, QUEUE_SORT_COLS)
  • Input validation: sort_dir validated against ('asc', 'desc') with safe default
  • Consistent pattern: Uses existing f-string pattern for dynamic ORDER BY clauses
  • Scope alignment: Directly addresses Issue [FEATURE] Sortable columns #111 (sortable columns)
  • UX polish: Visual indicators (arrows), three-state toggle (asc → desc → clear), hover effects
  • State preservation: Sort state preserved during pagination and filter changes
  • Documentation: Excellent CHANGELOG entry with clear user-facing description
  • Defensive coding: Orphan sorting handles None values gracefully with .get() and or ''

🚨 Issues Found

Severity File:Line Exact Code Quote Issue Fix
MEDIUM templates/library.html:325 let currentSort = '';
let currentSortDir = 'asc';
Sort state not synced from server on page load Initialize from response: `currentSort = data.sort

Explanation: When the page loads initially or after loadLibrary() completes, the JavaScript variables currentSort and currentSortDir are set to defaults ('' and 'asc'), but the server might return different values in the API response (lines 274-275 of diff show sort and sort_dir are returned). If a user navigates away and back, or if the page is refreshed with query params, the UI won't show the correct sort arrows until they interact with a column.

Code location for fix (library.html around line 402):

.then(data => {
    libraryData = data;
+   currentSort = data.sort || '';
+   currentSortDir = data.sort_dir || 'asc';
    updateCounts(data.counts);

📋 Scope Verification

Issue Problem Addressed? Notes
#111 Add sortable columns to library page Fully implemented: Author, Title, Status columns clickable with visual indicators, state preserved

Scope Status: SCOPE_OK

📝 Documentation Check

  • ✅ CHANGELOG.md: Updated with comprehensive description
  • ⚠️ README.md: Not updated - Consider adding sortable columns to feature list or UI documentation section (optional, not blocking)

🎯 Verdict

REQUEST_CHANGES

Fix required:

  1. Sync currentSort and currentSortDir from server response in loadLibrary() to ensure UI state matches backend state on page load

This is a well-crafted feature with strong security practices (SQL injection protection via whitelist). The single issue is a minor state synchronization bug that could cause UI confusion when users navigate or refresh the page. Once fixed, this is ready to ship.

@bucket-agent
Copy link

bucket-agent bot commented Feb 11, 2026

🔍 Vibe Check Review

Context

PR #146 adds sortable columns (Author, Title, Status) to the library page by implementing URL parameters for sort column/direction, with whitelist validation against SQL injection.

Codebase Patterns I Verified

SQL Query Patterns:

  • Standard pattern: c.execute('''SELECT ... WHERE ? ...''', (param,)) with parameterized queries (app.py:4866, 4876, 4882)
  • NO f-string usage in SQL found - verified with grep: c.execute(f''' returns 0 matches in current codebase
  • String interpolation in SQL does NOT exist as a pattern in this codebase

Error Handling:

  • Uses specific exceptions (ImportError, OSError, Exception as e) with logging (app.py:116-250)
  • Pattern: specific exceptions first, Exception as e as fallback
  • Always logs errors with context

Type Hints:

  • NOT consistently used (0 matches for -> Type in app.py)
  • Functions use docstrings instead of annotations

Security Patterns:

  • Whitelist validation exists: allowed_keys pattern found (app.py:10119-10122)
  • Used for validating API key names before operations

✅ Good

  1. Whitelist validation - BOOK_SORT_COLS, HISTORY_SORT_COLS, QUEUE_SORT_COLS prevent arbitrary column injection
  2. Direction validation - if sort_dir not in ('asc', 'desc'): sort_dir = 'asc' prevents SQL keyword injection
  3. Graceful fallback - When sort column not in whitelist, falls back to default ordering
  4. Consistent application - Applied to ALL filter contexts (pending, queue, fixed, verified, error, attention, locked, media types, search, all)
  5. Frontend UX - Three-state sort (asc → desc → clear) with visual indicators
  6. State preservation - Sort persists during pagination and filter changes
  7. Python client-side sorting - Orphans handled separately with list.sort() since they're not from SQL
  8. CHANGELOG - Excellent detailed documentation including SQL injection prevention mention

🚨 Issues Found

Severity Location Issue Fix
CRITICAL app.py:8966-8967 (diff lines 52-56) SQL INJECTION via f-string interpolation - build_order_by() uses f-string to insert {sort_cols[sort_by]} and {sort_dir.upper()} directly into SQL. While sort_by is validated against whitelist, the VALUES in sort_cols dict are RAW COLUMN NAMES that get f-string interpolated into the query string. This completely bypasses parameterized queries and creates SQL injection risk if a whitelist value is ever modified. Use a safer pattern: validate sort_by against whitelist, then use a conditional to select the ORDER BY string, OR ensure the entire ORDER BY clause is assembled BEFORE the f-string, OR use an allow-list of pre-built ORDER BY strings. The current pattern teaches dangerous SQL construction.
CRITICAL app.py:9079 (diff line 79) f-string SQL injection - c.execute(f'''SELECT ... {order} ...''') directly interpolates the order variable into SQL query string. Even with whitelist validation, this pattern is DANGEROUS and violates SQL parameterized query best practices. Never use f-strings with c.execute(). Build full query with conditionals OR use query composition tools. Current code pattern: order = build_order_by(...); c.execute(f'''... {order} ...''') violates every SQL injection prevention guideline.
HIGH app.py:8945-8967 No input length limits on sort parameter - malicious client could send extremely long sort column names, consuming memory in validation checks Add length check: sort_by = request.args.get('sort', '')[:50] before whitelist check
MEDIUM templates/library.html:309-316 Sortable column headers added to ALL columns including non-sortable ones - "Details" and "Actions" columns have no backend support Remove onclick handlers from non-sortable columns OR add backend support for all columns
MEDIUM app.py:9065-9069 (diff lines 65-69) Orphan sorting uses case-insensitive .lower() but SQL sorts are case-sensitive by default - inconsistent behavior between orphan view and other views Either: (1) Use SQL COLLATE NOCASE in ORDER BY clauses, OR (2) Document this as intended behavior
LOW app.py:8966-8967 Function build_order_by() defined inside route handler - creates new function object on every request Move function outside route handler to module level or use a class method

Quote verification for CRITICAL issues:

# app.py diff lines 52-56 (CRITICAL ISSUE #1):
    def build_order_by(sort_cols, default_order):
        """Build ORDER BY clause from user sort or fall back to default."""
        if sort_by and sort_by in sort_cols:
            return f"ORDER BY {sort_cols[sort_by]} {sort_dir.upper()}"  # <-- f-string SQL
        return f"ORDER BY {default_order}"
# app.py diff lines 78-86 (CRITICAL ISSUE #2 example):
        order = build_order_by(HISTORY_SORT_COLS, 'h.fixed_at DESC')
        c.execute(f'''SELECT h.id, h.book_id, h.old_author, h.old_title, h.new_author, h.new_title,
                            h.old_path, h.new_path, h.status, h.fixed_at, h.error_message,
                            b.path, b.current_author, b.current_title
                     FROM history h
                     JOIN books b ON h.book_id = b.id
                     WHERE h.status = 'pending_fix'
                     {order}  # <-- Direct interpolation into query string
                     LIMIT ? OFFSET ?''', (per_page, offset))

Why this is CRITICAL:

The codebase has ZERO instances of c.execute(f''' or c.execute(f" - this PR introduces a completely new anti-pattern. While the whitelist prevents direct user input from reaching the column name, the pattern itself:

  1. Breaks parameterized queries - The ORDER BY clause is not a parameter, it's string interpolation
  2. Creates maintenance risk - Future developers might copy this pattern for WHERE clauses
  3. Violates SQLite best practices - SQLite documentation explicitly warns against string formatting in SQL
  4. Is unnecessary - Can be solved with a dictionary lookup or conditional logic without f-strings

📋 Scope Verification

Issue Problem Addressed? Notes
#111 Add sortable columns to library interface Author, Title, Status columns are sortable with visual feedback and state persistence

Scope Status: SCOPE_OK

Feature fully implements the requested sortable columns functionality.

📝 Documentation Check

This is a feat: PR

  • CHANGELOG.md: ✅ Updated with detailed feature description including SQL injection prevention note
  • README.md: ⚠️ Consider updating - This is a significant UI improvement worth mentioning in the feature list

🎯 Verdict

REQUEST_CHANGES

BLOCKING ISSUE: SQL Injection Anti-Pattern

This PR introduces f-string SQL query construction (c.execute(f'''... {variable} ...''')) into a codebase that currently has ZERO instances of this pattern. While the whitelist validation prevents direct exploitation, the pattern itself is:

  1. Dangerous precedent - Sets example for future code
  2. Against SQLite best practices - String interpolation in SQL is explicitly discouraged
  3. Unnecessary - Can be solved safely with conditional logic or dictionary lookup

Required fixes before merge:

  1. CRITICAL: Remove ALL f-string usage in c.execute() calls

    • Replace with: conditional ORDER BY selection, or pre-built query dictionary, or query builder pattern
    • Example safe pattern:
    # Safe approach - no f-strings
    order_clauses = {
        'author_asc': 'ORDER BY current_author ASC',
        'author_desc': 'ORDER BY current_author DESC',
        'title_asc': 'ORDER BY current_title ASC',
        # ... etc
    }
    order_key = f"{sort_by}_{sort_dir}" if sort_by in BOOK_SORT_COLS else 'default'
    order_clause = order_clauses.get(order_key, 'ORDER BY updated_at DESC')
    
    c.execute('''SELECT ... WHERE status = 'verified' ''' + order_clause + ''' LIMIT ? OFFSET ?''', (per_page, offset))
  2. HIGH: Add input length validation (sort_by = request.args.get('sort', '')[:50])

  3. MEDIUM: Remove onclick from non-sortable "Details" and "Actions" column headers

Non-blocking improvements:

  • Move build_order_by() outside route handler
  • Add COLLATE NOCASE to SQL ORDER BY for consistency with orphan sorting
  • Update README with new feature

The feature implementation is excellent, but the SQL construction pattern cannot be merged as-is. Fix the f-string issue and this will be ready to ship.

)

Eliminates all f-string usage in c.execute() calls. ORDER BY clauses
now built via string concatenation from whitelisted dict values.
Adds input length validation on sort parameter.
@bucket-agent
Copy link

bucket-agent bot commented Feb 11, 2026

🔍 Vibe Check Review

Context

PR #146 adds sortable columns (Author, Title, Status) to the library table. Users can click headers to toggle sort direction (asc → desc → clear). Backend implements whitelist-based SQL ORDER BY clause construction to prevent injection.

Codebase Patterns I Verified

I examined existing patterns in app.py to understand the project's conventions:

  • SQL queries: All existing queries use parameterized queries (? placeholders) for user input, NOT string formatting
  • ORDER BY clauses: Currently hardcoded literals like ORDER BY h.fixed_at DESC throughout app.py (lines 6789, 6926, 8103, 9060, etc.)
  • Request params: Use request.args.get() with type coercion and defaults (e.g., page = request.args.get('page', 1, type=int))
  • String truncation: PR uses sort_by = request.args.get('sort', '')[:20] - I found this pattern is unusual for this codebase; most params don't have length limits

✅ Good

  1. SQL injection protection: Whitelist approach (BOOK_SORT_COLS, HISTORY_SORT_COLS, QUEUE_SORT_COLS) correctly prevents arbitrary column injection
  2. Direction validation: sort_dir is validated against ('asc', 'desc') before use
  3. Helper function: build_order_by() centralizes ORDER BY construction logic
  4. State preservation: Frontend correctly includes sort params in pagination/filter URLs
  5. Visual feedback: Sort icons and hover effects properly implemented
  6. Orphan sorting: In-memory Python sorting for orphans (which aren't in DB) is handled correctly
  7. CHANGELOG updated: Comprehensive entry explains the feature and security measures
  8. Scope alignment: Directly addresses Issue [FEATURE] Sortable columns #111's request for sortable columns

🚨 Issues Found

Severity Location Exact Code Quote Issue Fix
LOW app.py:8956 sort_by = request.args.get('sort', '')[:20] Arbitrary 20-char limit has no justification; longest column key is "priority" (8 chars). Truncation could silently break valid input. Remove [:20] truncation since whitelist already validates input, OR increase to reasonable value like [:30] if paranoid
LOW templates/library.html:298-304 <th style="width: 23%; cursor: pointer; user-select: none;" onclick="toggleSort('author')" Column headers hardcode sort keys in onclick handlers. If backend whitelist changes, frontend requires manual sync. Consider: This is acceptable for a small, stable feature. Not worth blocking over.

📋 Scope Verification

Issue Problem Addressed? Notes
#111 Add ability to sort columns in interface Fully implemented: Author, Title, and Status columns are now sortable with visual indicators. Three-click cycle (asc → desc → clear) works correctly.

Scope Status: SCOPE_OK

📝 Documentation Check

  • CHANGELOG.md: ✅ Updated with detailed entry explaining feature and security measures
  • README.md: N/A - Feature doesn't require README updates (UI-only enhancement)

🎯 Verdict

APPROVE

This is solid work. The SQL injection protection is correctly implemented via whitelist, and the feature fully addresses Issue #111. The two LOW-severity issues I found are nitpicks that don't warrant blocking:

  1. The [:20] truncation is overly cautious but harmless since the whitelist validation happens anyway
  2. The frontend/backend coupling of sort keys is typical for this type of feature

The code is safe to ship. The developer clearly thought about security (explicit mention in CHANGELOG, whitelist approach) and implemented comprehensive sorting across all filter contexts.

Optional improvements for future consideration (not blocking):

  • Remove the [:20] truncation since it serves no purpose with whitelist validation
  • Add a comment above the whitelist dicts explaining why each column name maps to its SQL expression

@deucebucket deucebucket merged commit 91c90a7 into develop Feb 11, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant