Skip to content

Release v0.6.4#371

Merged
masonfox merged 345 commits intomainfrom
develop
Feb 16, 2026
Merged

Release v0.6.4#371
masonfox merged 345 commits intomainfrom
develop

Conversation

@masonfox
Copy link
Owner

@masonfox masonfox commented Feb 16, 2026

Fix AUTOINCREMENT Sequence Leak (PR #370)

Critical bug fix that prevents SQLite AUTOINCREMENT sequence from leaking during Calibre sync operations.

The Problem:

  • Drizzle ORM's onConflictDoUpdate() was incrementing the sequence on every sync, even for UPDATE operations
  • Production database had 894 books with IDs 1-636,267, but sequence was at 645,512
  • 99.86% ID waste (644,618 leaked IDs)

The Solution:

  • Refactored to split INSERT/UPDATE operations with dedicated bulkInsert() and bulkUpdate() methods
  • Eliminated ~850 redundant database queries per sync
  • Better separation of concerns (sync service orchestrates, repository executes)

Impact:

  • Fresh database stays stable across syncs (sequence no longer leaks)
  • Performance improvement from eliminating redundant queries
  • Comprehensive regression test suite added (5 new tests, all 3,822 tests passing)

Full details: #370

masonfox and others added 30 commits December 26, 2025 12:01
## Summary

Adds the ability to view progress logs from inactive
(completed/archived) reading sessions by making the progress summary
panel clickable in the Reading History section.

## Changes

### New Components
- **SessionProgressModal**: Responsive modal component that displays as
a full-screen modal on desktop and full-height bottom sheet on mobile
- **useSessionProgress**: Custom hook for fetching session progress logs
with TanStack Query caching

### Enhanced Components
- **BottomSheet**: Added `size` configuration (`default` | `full`) and
`allowBackdropClose` option
- Full-size bottom sheets now have fixed headers with scrollable content
- **ReadingHistoryTab**: Progress summary panel is now clickable with
hover effects and visual affordance (chevron icon)

### User Experience

**Desktop:**
- Click progress summary panel → Modal opens with progress logs
- Large 2xl modal with scrollable content area
- ESC key or Close button to dismiss

**Mobile:**
- Tap progress summary panel → Full-height bottom sheet slides up
- Fixed header, scrollable content
- Close button only (no backdrop/swipe dismissal)

### Technical Details

- Reuses existing `JournalEntryList` component for consistency
- No API changes needed (existing `?sessionId=X` param already
supported)
- View-only access (no editing for inactive sessions)
- Loading, error, and empty states handled
- Responsive viewport detection (768px breakpoint)
- Query caching prevents redundant API calls

## Testing

- ✅ Linting passed
- ✅ Production build successful
- ✅ Desktop and mobile responsive behavior
- ✅ Loading/error/empty states
- ✅ Keyboard accessibility (ARIA labels)

## Visual Changes

Progress summary panel now shows hover state with accent border and
chevron indicator, making it clear the panel is clickable.
)

## Summary

- Fix modal dismissal bug where FinishBookModal wouldn't close on book
detail page after clicking "Mark as Read"
- Ensure consistent modal behavior across dashboard and book detail page
contexts

## Problem

The FinishBookModal was not dismissing on the book detail page
(`/books/:id`) when clicking "Mark as Read", while it worked correctly
on the dashboard. This created an inconsistent UX where users had to
manually close the modal after marking a book as read.

## Root Cause

The `handleSubmit` function in `FinishBookModal.tsx` called
`onConfirm()` but did not call `onClose()`, relying entirely on the
parent component to update the `isOpen` prop. This worked in some
contexts (like `LogProgressModal`) where the parent explicitly closed
both modals, but failed in the book detail page context where the modal
state update had timing issues with async mutations.

## Solution

Modified `FinishBookModal.handleSubmit` to:
1. Make the function async to properly await `onConfirm()` completion
2. Explicitly call `onClose()` after successful confirmation
3. This ensures consistent dismissal behavior across all usage contexts

## Changes

**`components/FinishBookModal.tsx`:**
- Made `handleSubmit` async
- Added explicit `onClose()` call after `onConfirm()` completes

**`__tests__/components/FinishBookModal.test.tsx`:**
- Updated test to handle async behavior with `waitFor()`
- Made mock `onConfirm` async to match real implementation

## Testing

- ✅ All 26 FinishBookModal tests passing
- ✅ Linter passes with no errors
- ✅ Test coverage maintained at 100% for FinishBookModal

## Verification

To test the fix:
1. Navigate to a book detail page with a book in "reading" status
2. Change the status dropdown to "Read"
3. Fill in rating/review in the FinishBookModal
4. Click "Mark as Read"
5. ✅ Modal should now dismiss automatically

## Related Issues

Fixes user-reported bug where modal wouldn't dismiss on book detail
page.
## Summary

- Changes tag filtering from OR logic to AND logic when multiple tags
are selected
- Books must now have ALL selected tags instead of ANY of them
- Adds comprehensive test coverage with 21 new repository-level tests

## Changes Made

### Code Changes
- **`lib/repositories/book.repository.ts`**: Updated both
`findWithFilters` and `findWithFiltersAndRelations` methods to use
`and(...tagConditions)` instead of `or(...tagConditions)`
- Added inline comments to clarify the AND logic behavior

### Test Updates
- **`__tests__/api/books.test.ts`**: Updated existing OR logic test to
AND logic, added 4 new comprehensive tests
- **`__tests__/lib/search.test.ts`**: Updated existing OR logic test to
AND logic, added 5 new edge case tests
- **`__tests__/repositories/book-tag-filtering.test.ts`**: New file with
21 comprehensive repository-level tests

## Test Coverage

All 1553 tests pass including:
- Single tag filtering (backward compatibility)
- Multiple tag AND logic (core fix)
- Edge cases (empty tags, case sensitivity, duplicates)
- Combined filters (tags + search)
- Pagination with tag filters
- Real-world scenarios

## Example Behavior Change

**URL:** `/library?tags=Action+%26+Adventure,Civil+war`

**Before (OR logic):**
- Shows books with "Action & Adventure" OR "Civil war"
- Less precise filtering

**After (AND logic):**
- Shows only books with BOTH "Action & Adventure" AND "Civil war"  
- More precise filtering that matches user expectations

Closes #167
## Summary

Implements **#143**: Add tag editing functionality with bi-directional
sync between Tome and Calibre databases.

## Features

### Tag Management
- ✅ View and edit tags from book detail page
- ✅ Search from 735+ existing library tags with autocomplete
- ✅ Create new tags by typing and pressing Enter
- ✅ Remove tags by clicking X on tag pills
- ✅ Batched saves with Cancel option

### Bi-directional Sync
- ✅ Writes to Calibre database first (fail-fast strategy)
- ✅ Updates Tome database on success
- ✅ Reuses existing tags (no duplicates in Calibre)
- ✅ Validates input (trim whitespace, filter empty strings)
- ✅ Optimistic UI updates for instant feedback

### UX & Polish
- ✅ Large modal (max-w-4xl) for better visibility
- ✅ Fixed dropdown positioning to prevent scroll conflicts
- ✅ Edit button with "Edit" text + Pencil icon on right side
- ✅ Responsive design (mobile + desktop)
- ✅ Accessible (aria-labels, keyboard navigation)
- ✅ Loading states and error handling with toasts

## Implementation

### Backend (3 commits)
1. **Database layer**: `updateCalibreTags()` and `readCalibreTags()` in
`lib/db/calibre-write.ts`
2. **Service layer**: `BookService.updateTags()` with Calibre sync-first
pattern
3. **API endpoint**: `PATCH /api/books/:id/tags` with full validation
4. **Bug fix**: Wrapped `revalidatePath()` in try-catch for test
environments

### Frontend (2 commits)
1. **Modal component**: `TagEditor` with search, autocomplete, and tag
management
2. **Hook integration**: Added `updateTags` mutation to `useBookDetail`
hook
3. **Page integration**: Added edit button and modal to book detail page

### UX Refinements (3 commits)
1. Fixed modal scrolling issues (max-w-4xl, fixed dropdown positioning)
2. Switched to Pencil icon for consistency
3. Final button design: "Edit" + Pencil icon on right side

## Testing

- **1568/1568 tests passing** ✅
- **66 new tests** added:
  - 48 unit tests for database operations
  - 18 API integration tests
- **Manual testing**: All flows verified ✓
  - Adding tags
  - Removing tags
  - Creating new tags
  - Syncing to Calibre database

## Files Changed

### Backend (5 files, +815 lines)
- `lib/db/calibre-write.ts` - Tag CRUD operations (+113)
- `lib/services/book.service.ts` - Service layer method (+58)
- `app/api/books/[id]/tags/route.ts` - API endpoint (new, 94 lines)
- `app/api/books/[id]/rating/route.ts` - revalidatePath fix (+9)
- `__tests__/` - Unit and integration tests (+644)

### Frontend (3 files, +316 lines)
- `components/BookDetail/TagEditor.tsx` - Modal component (new, 249
lines)
- `hooks/useBookDetail.ts` - Mutation hook (+45)
- `app/books/[id]/page.tsx` - Page integration (+27)

## Checklist

- [x] All tests passing (1568/1568)
- [x] No regressions
- [x] Follows existing code patterns
- [x] Accessible (keyboard navigation, aria-labels)
- [x] Responsive (mobile + desktop)
- [x] Manual testing complete
- [x] Documentation in code (JSDoc comments)

Closes #143
## Summary

- Fixes critical bug where tag renames (especially case-only changes)
don't persist after page refresh
- Adds comprehensive debug logging for all tag operations to improve
observability

## Details

### Bug Fixes

**Tag Rename Race Condition:**
- Tag renames would appear successful but revert on page refresh
- Root cause 1: Calibre file watcher was syncing back old data after
rename completed
- Root cause 2: Calibre's `tags` table uses `COLLATE NOCASE` for
case-insensitive storage, but our code was doing case-sensitive lookups
- Solution: Suspend watcher during operations + use case-insensitive tag
lookup + update tag case when it changes

**Watcher Debounce Issue:**
- Suspending watcher didn't clear pending debounce timer
- Solution: Clear timer when suspending to prevent queued events from
firing

### Enhancements

**Debug Logging:**
- Added operation-specific logging prefixes: `[RENAME]`, `[MERGE]`,
`[DELETE]`, `[BULK_DELETE]`, `[WATCHER]`
- Detailed step-by-step debug logs for troubleshooting
- Completion messages at INFO level
- Consistent logging pattern across all tag operations

## Files Changed

- `lib/calibre-watcher.ts` - Improved suspend/resume + detailed logging
- `lib/db/calibre-write.ts` - Case-insensitive tag lookup + case update
logic
- `lib/services/book.service.ts` - Debug logging for all tag operations

## Testing

- ✅ All 56 tests pass
- ✅ Linting clean
- ✅ TypeScript compilation successful
## Problem

Fixes #181

Users accessing Tome over HTTP in production mode (e.g., local networks,
Docker deployments without TLS) cannot log in. The authentication
cookie's `secure` flag was hardcoded to `process.env.NODE_ENV ===
"production"`, which assumes production deployments always use HTTPS.

When `secure: true` is set on a cookie over HTTP (except
localhost/127.0.0.1), browsers reject the cookie. This caused:
- **Correct password**: Page stays on login screen (cookie rejected,
redirect loop)
- **Incorrect password**: Error shown correctly (no cookie involved)

The issue was reproduced with a user accessing Tome at
`http://192.168.1.158:3000` in production mode.

## Solution

Auto-detect the connection protocol from the request instead of relying
on `NODE_ENV`:

1. Added `isSecureConnection()` helper that checks:
- `x-forwarded-proto` header first (for reverse proxies/load balancers)
   - Request URL protocol as fallback
2. Updated `createAuthResponse()` to accept the request and use protocol
detection
3. Updated login route to pass request to `createAuthResponse()`

## Changes

- `lib/auth.ts` - Added protocol detection logic and updated
`createAuthResponse()` signature
- `app/api/auth/login/route.ts` - Pass request to `createAuthResponse()`
- `__tests__/lib/auth.test.ts` - Added 4 comprehensive tests covering
HTTP, HTTPS, and proxy scenarios
- `package.json` - Bump version to 0.3.1

## Testing

- All tests passing ✓
- New tests verify correct behavior for:
  - Direct HTTPS requests (secure: true)
  - Direct HTTP requests (secure: false)
  - HTTP requests with `x-forwarded-proto: https` (secure: true)
  - HTTP requests with `x-forwarded-proto: http` (secure: false)

## Test Plan

- [ ] Deploy to production mode
- [ ] Access over HTTP (e.g., `http://192.168.1.x:3000`)
- [ ] Verify login works with correct password
- [ ] Verify error shows with incorrect password
- [ ] If using reverse proxy, verify `x-forwarded-proto` header is
respected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
## Summary

This PR implements a comprehensive refactoring of book completion
workflows, introducing auto-completion at 100% progress, a new API
client architecture, and service layer consolidation. The changes fix
critical bugs with backdated completions, eliminate code duplication,
and establish patterns for future development.

## Key Features

### 1. Auto-Completion at 100% Progress ✨
**Fixes #175** - Books automatically transition to "read" status when
progress reaches 100%, using the progress date (not today's date) as the
completion date.

**Benefits:**
- Fixes backdated completion bug
- More semantic: 100% progress = finished
- Single source of truth for completion
- Eliminates duplicate completion flows

### 2. Hybrid API Client Architecture 🏗️
**New:** Type-safe API client layer with domain-based organization

**Added:**
- `BaseApiClient` with comprehensive error handling, timeouts, type
safety
- `bookApi` domain helper with typed methods
- `ApiError` class with structured error details
- Foundation for migrating 38 endpoints across 12 domains

**Benefits:**
- Full TypeScript type safety for all API calls
- IDE autocomplete for methods and request/response shapes
- Standardized error handling
- Easier to test (mock `bookApi` instead of `fetch`)
- Single place for auth headers, retry logic, interceptors

**Architecture:**
```
React Hooks → Domain API Objects (bookApi) → BaseApiClient → Fetch API
```

See **ADR-010** for detailed rationale.

### 3. Service Layer Consolidation 🔧
**Major refactoring:** Moved all book status transition logic into
`SessionService`

**Changes:**
- Added composable methods to `SessionService`:
  - `ensureReadingStatus()` - Status transitions
  - `create100PercentProgress()` - Progress with auto-completion
  - `updateBookRating()` - Rating updates with Calibre sync
  - `updateSessionReview()` - Review updates
  - `markAsRead()` - Unified orchestration method
- Simplified `useBookStatus` hook from 534 to 270 lines (49% reduction)
- Created `/api/books/:id/mark-as-read` endpoint
- Updated rating API to use service layer

**Benefits:**
- Business logic independently testable without React
- API routes and UI hooks use same validated logic
- Future features (CLI tools) can reuse methods
- Clearer separation: service = business logic, hook = UI concerns

See **ADR-011** for comprehensive documentation.

### 4. Complete Book Workflow 📚
**New:** Dedicated workflow for marking books as read from Want to
Read/Read Next

**Added:**
- `CompleteBookModal` - Collect all data upfront (dates, pages, rating,
review)
- `/api/books/:id/complete` endpoint - Single transaction for complete
workflow
- Separates "complete from non-reading" vs "finish currently reading"
flows

**Benefits:**
- Better UX for each scenario
- All completion data in one step
- Consistent behavior across status transitions

## Bug Fixes

1. ✅ Backdated completion dates now preserved correctly (#175)
2. ✅ Reviews saved to reading session table (not book table)
3. ✅ 100% progress log created when marking as read from any status
4. ✅ Duplicate progress creation prevented in FinishBookModal
5. ✅ Reading history refreshes immediately after auto-completion
6. ✅ Client-side bundling issue resolved (no server imports in hooks)
7. ✅ Skip button in modal now properly marks as read without rating

## User Flows

### Scenario 1: Log Progress to 100% (with backdating)
```
1. User logs progress to 100% with Dec 15 date
2. ✅ Progress service auto-changes status to "read" with completedDate = Dec 15
3. Modal appears for optional rating/review
4. User adds rating → saved to session
5. ✅ completedDate remains Dec 15 (BUG FIXED!)
```

### Scenario 2: Mark as Read from Want to Read
```
1. User clicks status dropdown → "Read"
2. CompleteBookModal appears with all fields
3. User enters start date, end date, pages read, rating, review
4. All saved in single transaction
5. Book marked as read with correct dates
```

### Scenario 3: Manual "Mark as Read" from Reading
```
1. User clicks "Read" status while actively reading
2. System creates 100% progress (auto-completes)
3. FinishBookModal appears for rating/review
4. User can skip or add rating/review
```

## Architecture Changes

### New Files Created
- `lib/api/base-client.ts` - Base API client with error handling
- `lib/api/domains/book/api.ts` - Book domain API methods
- `lib/api/domains/book/types.ts` - TypeScript types
- `app/api/books/[id]/complete/route.ts` - Complete book endpoint
- `app/api/books/[id]/mark-as-read/route.ts` - Mark as read endpoint
- `components/CompleteBookModal.tsx` - New completion modal

### Enhanced Components
- `useBookStatus.ts` - Simplified by 49% (534 → 270 lines)
- `session.service.ts` - Added 9 new composable methods
- `progress.service.ts` - Auto-completion logic
- `FinishBookModal.tsx` - Updated UX (title, button text)

### Test Coverage
- **Added:** 62 new/updated tests
- **Total:** 1,848 tests passing (up from 1,830)
- **Coverage:** SessionService 100% functions, 95.73% lines
- **Coverage:** BaseApiClient 92%

## Documentation

### New ADRs
- **ADR-010:** Hybrid API Client Architecture
  - Decision rationale and alternatives considered
  - Architecture patterns and benefits
  - Migration strategy for remaining endpoints

- **ADR-011:** Status Transition Service Consolidation
  - Complete refactoring documentation
  - Phase-by-phase breakdown with commit references
  - Architecture diagrams and metrics
  - Migration guide for developers

### New Test Documentation
- **AUTO_COMPLETION_TEST_PLAN.md** - 34+ test scenarios
- **AUTO_COMPLETION_TEST_SUMMARY.md** - Executive summary

## Breaking Changes

None - all existing flows maintained with backward compatibility.

## Testing

```bash
# Run full test suite
bun test

# Build verification
bun run build
```

## Metrics

- **Code Reduction:** 49% in useBookStatus hook (534 → 270 lines)
- **Code Addition:** 6,308 insertions (includes tests + docs)
- **Test Growth:** +62 new/updated tests (+18 from 1,830 to 1,848)
- **Files Changed:** 32 files
- **Commits:** 20 commits across 4 major refactoring phases

## Future Enhancements

**Backdating Modal for Historical Books** (tracked separately):
- When marking book as read with NO existing progress
- Allow user to specify start/end dates explicitly
- Useful for importing historical reading data

## Related Issues

Fixes #175

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
#191)

## Summary
Fixes a follow-up issue from PR #190 where users still couldn't create a
2026 goal after the page was fixed to default to showing 2025.

## Problem
After PR #190 made the page default to showing 2025 (the most recent
year with a goal), a new issue emerged:
- Users viewing 2025 couldn't create a goal for 2026
- The year dropdown only showed years that already had goals
- The form didn't respect the selected year from the parent component

## Root Cause
1. `availableYears` only included years from existing goals (2026 was
missing from dropdown)
2. `ReadingGoalForm` always used `currentYear` for create mode, ignoring
UI context
3. User saw "Create Goal for 2025" but form tried to create for 2026 (or
vice versa)

## Solution
- **Always include current year in dropdown**, even if no goal exists
yet
- **Pass `selectedYear` prop to form** to respect user's year selection
- **Update validation** to only block future years (allows current/past
years per API)

## Changes
- `components/GoalsPagePanel.tsx`: 
  - Modified `availableYears` to always include current year
  - Pass `selectedYear` to `ReadingGoalForm`
- `components/ReadingGoalForm.tsx`:
  - Accept `selectedYear` prop
  - Use it for create mode instead of always defaulting to `currentYear`
  - Fix validation to match API (block future years only)

## User Flow (Fixed)
1. User lands on /goals → sees 2025 (has goal)
2. Dropdown shows both 2025 and 2026
3. User selects 2026 → sees "No goal set for 2026"
4. Clicks "Create Goal for 2026" → modal opens with correct year
5. Form submits for 2026 ✅

## Related
- Depends on: #190
- Fixes regression introduced by: #190
## Summary

- Remove Pino logger from middleware to resolve production 500 errors
when `LOG_DEST` environment variable is set
- Simplify middleware to focus solely on authentication logic

## Problem

When `LOG_DEST` is set to a file path in production, the application
returns 500 errors with the following:

```
TypeError: e$().destination is not a function
```

This occurs because:
1. Pino's `destination()` function requires Node.js filesystem APIs
2. Next.js middleware runs in the Edge Runtime by default (even in
Next.js 16)
3. Edge Runtime does not support Node.js `fs` module operations
4. The logger module attempts to create file destinations at module load
time, causing the bundle to fail

## Solution

Remove logger from middleware entirely because:
- Middleware only had 2 debug-level log statements
- Authentication flow can be observed through HTTP status codes and
container logs
- Middleware should be lightweight (Next.js best practice)
- API routes and server components still have full logging with file
destination support

## Future Work

During the Next.js 16 migration, we can:
- Migrate from `middleware.ts` to `proxy.ts` (new convention)
- Optionally configure Node.js runtime for proxy if logging is truly
needed
- Evaluate if proxy logging provides enough value to justify the
performance trade-off

## Testing

- [x] Removed logger imports and calls from middleware.ts
- [x] Verified middleware logic remains intact
- [x] Confirmed change only affects middleware (API routes retain full
logging)

## Files Changed

- `middleware.ts`: Removed logger import and debug calls, simplified
auth flow
## Summary

Standardizes loading states across all tag management modals on the
`/tags` page, fixing a critical bug where modals would close immediately
before async operations completed, preventing users from seeing loading
feedback.

## Changes

### 🐛 Critical Bug Fix
- **Fixed**: Modals were closing immediately when users clicked confirm,
before async operations completed
- **Result**: Modals now stay open during async operations and display
loading spinners
- Removed premature `onClose()` calls from all modal confirm handlers

### ✨ Loading State Improvements
- **Loading button text**: Buttons now show dynamic text during
operations:
  - "Delete Tag" → "Deleting..."
  - "Merge Tags" → "Merging..."
  - "Rename" → "Renaming..."
  - "Remove Tag" → "Removing..."
  - "Delete X Tags" → "Deleting..."
- **Disabled buttons during loading**: Cancel and X buttons are disabled
to prevent dismissal mid-operation
- **Loading spinner**: BaseModal displays spinner in body when
`loading={true}`

### 🔄 Error Handling
- Modals now stay open when errors occur
- Users can see error toast and retry the operation
- Modal only closes on successful completion
- Moved modal close logic from `finally` block into `try` block (only on
success)

### 📦 RemoveTagFromBook Modal
- Fixed async handling in both `TagDetailPanel` and
`TagDetailBottomSheet`
- Each `BookCardSimple` component manages its own loading state
- Properly handles async removal with error recovery
- Modal stays open if removal fails, allowing retry

## Files Modified (9 files)

- `components/BaseModal.tsx` - Disabled X button during loading
- `components/TagManagement/DeleteTagModal.tsx` - Removed premature
close, added loading text
- `components/TagManagement/MergeTagsModal.tsx` - Removed premature
close, added loading text
- `components/TagManagement/RenameTagModal.tsx` - Removed premature
close, added loading text
- `components/TagManagement/RemoveTagFromBookModal.tsx` - Removed
premature close, added loading text
- `components/TagManagement/BulkDeleteTagsModal.tsx` - Added loading
text, disabled cancel button
- `components/TagManagement/TagDetailPanel.tsx` - Added async handling
for book card removal
- `components/TagManagement/TagDetailBottomSheet.tsx` - Added async
handling for book card removal
- `app/tags/page.tsx` - Updated 4 handlers to only close modals on
success

## User Experience

### Before
- User clicks "Delete Tag"
- Modal closes immediately
- User sees no feedback
- Tag disappears after network request completes (if successful)
- No way to retry on error

### After
- User clicks "Delete Tag"
- Button text changes to "Deleting..."
- Button is disabled
- Cancel/X button is disabled
- Modal body shows loading spinner
- On success: Modal closes, success toast appears
- On error: Modal stays open, error toast appears, user can retry

## Testing

Manual testing recommended for:
- [ ] Delete tag - verify spinner shows, modal closes on success
- [ ] Merge tags - verify spinner shows, modal closes on success
- [ ] Rename tag - verify spinner shows, modal closes on success
- [ ] Bulk delete tags - verify spinner shows, modal closes on success
- [ ] Remove tag from book - verify spinner shows in both panel and
bottom sheet
- [ ] Error cases - disconnect network, verify modal stays open and user
can retry

## Related

Addresses feedback about inconsistent loading states on the `/tags`
page, especially for modals.
## Summary

- Implement multi-stream logging to write logs to both stdout and file
destinations
- Fixes issue where logs were only visible in log files but not in
Portainer container logs

## Problem

When `LOG_DEST` is configured, Pino was writing logs exclusively to the
file destination, which meant:
- ❌ Logs were NOT visible in Portainer (which shows stdout/stderr)
- ✅ Logs were only available in the file

This made real-time monitoring difficult since you had to access the log
file directly instead of using the container log viewer.

## Solution

Configure Pino to use `multistream()` to write to **both** stdout and
file simultaneously:

1. **Always** write to stdout (for container log viewers like Portainer)
2. **Conditionally** write to file when `LOG_DEST` is set (for
persistence)

This is the industry-standard approach for production logging systems.

## Changes

- Updated `lib/logger.ts` to use `pino.multistream()`
- Created array of stream destinations:
  - stdout: Always included
  - file: Included when `LOG_DEST` env var is set
- Maintains backward compatibility with all existing logging features

## Benefits

✅ **Real-time visibility**: Logs appear in Portainer/Docker logs
instantly
✅ **Persistent logs**: File logging still works for historical analysis
✅ **No trade-offs**: Get both stdout and file logging simultaneously  
✅ **Standard practice**: This is how most production systems handle
logging
✅ **Zero breaking changes**: All existing functionality preserved

## Testing

- [x] Tested locally with `LOG_DEST=/tmp/test.log`
- [x] Verified logs appear on stdout
- [x] Verified logs are written to file
- [x] Server starts without errors

## Configuration

No changes needed - this works with existing env vars:
- `LOG_DEST`: Optional file path for persistent logs
- `LOG_LEVEL`: Controls verbosity (trace, debug, info, warn, error)
- `LOG_PRETTY`: Pretty-print logs in development
- `LOG_ENABLED`: Enable/disable logging

## Example Output

**Stdout (visible in Portainer):**
```json
{"level":"info","time":1767364620665,"msg":"Starting application..."}
```

**File (persistent):**
```json
{"level":"info","time":1767364620665,"msg":"Starting application..."}
```

Both destinations receive identical log entries.
Fix production error where pino.multistream() was being called in browser bundles,
causing 'multistream is not a function' errors. Pino's multistream is a Node.js-only
feature and cannot be used in client-side code.

Changes:
- Detect server environment (Node.js/Bun) vs browser using process.versions.node
- Only use pino.multistream() on server-side when LOG_DEST is configured
- Fall back to standard pino logger for server without LOG_DEST
- Use pino browser mode for client-side (logs to console)

This ensures:
- Server-side: Logs go to both stdout (Portainer) AND file (when LOG_DEST set)
- Client-side: Logs work in browser without errors
- No breaking changes to existing functionality
Updated funding options and added Buy Me a Coffee username.
## Summary

Implements comprehensive partial success feedback for tag operations,
allowing operations to continue and report detailed results even when
some Calibre sync failures occur.

Previously, if 84 of 85 tag updates succeeded, the entire operation
would fail with a generic error. Now, users see detailed feedback
showing which books succeeded, which failed, and why.

## Changes

### Backend (Calibre & Service Layer)
- **`lib/db/calibre-write.ts`**: Updated `batchUpdateCalibreTags()` to
return `CalibreBatchResult` with detailed failure information instead of
just a success count
- **`lib/services/calibre.service.ts`**: Updated interface to reflect
new return type
- **`lib/services/tag.service.ts`**: Refactored all tag operations to:
  - Continue with Tome DB updates even when some Calibre updates fail
  - Collect and enrich failure details with book titles for better UX
- Return `TagOperationResult` with comprehensive success/failure
breakdown
  - Only throw errors when ALL operations fail (not partial failures)

### Backend (API Routes)
Updated all tag operation endpoints to return new response structure:
- **`app/api/tags/merge/route.ts`** - Tag merge operations
- **`app/api/tags/[tagName]/route.ts`** - Rename (PATCH) and delete
(DELETE) operations
- **`app/api/tags/bulk-delete/route.ts`** - Bulk delete operations

New response format:
```typescript
{
  success: boolean,              // true if no failures
  partialSuccess: boolean,       // true if some succeeded, some failed
  totalBooks: number,
  successCount: number,
  failureCount: number,
  calibreFailures?: Array<{ calibreId, bookId?, title?, error }>,
  tomeFailures?: Array<{ bookId, error }>,
  // operation-specific fields...
}
```

### Frontend (UI Components)
- **`types/tag-operations.ts`**: Added shared TypeScript types for
operation results
- **`components/TagManagement/TagOperationResults.tsx`**: New component
for displaying detailed operation results with:
  - Success/partial success/failure status indicators
  - Expandable list of failed books with titles and error messages
  - "Copy Error Report" button for troubleshooting
  - Informative notes about successful vs failed updates

- **Updated all modal components** to support "results mode":
  - `MergeTagsModal.tsx`
  - `RenameTagModal.tsx`
  - `DeleteTagModal.tsx`
  - `BulkDeleteTagsModal.tsx`
  
  Each modal now:
  - Accepts `result` prop to display operation results inline
  - Switches between "input mode" and "results mode"
  - Keeps modal open to show detailed results on partial success
  - Changes action buttons to "Close" when showing results

- **`hooks/useTagManagement.ts`**: Updated to return
`TagOperationResult` and refresh data even on partial success

- **`app/tags/page.tsx`**: Implemented smart toast notifications:
  - ✅ **Full success**: Green toast + auto-close modal
- ⚠️ **Partial success**: Yellow warning toast + keep modal open with
details
- ❌ **Complete failure**: Red error toast + keep modal open with details

### Tests
Updated 7 test files to validate new behavior:
- `__tests__/lib/calibre-write.test.ts` - Calibre layer tests
- `__tests__/services/tag.service.test.ts` - Service layer tests
- `__tests__/api/tags.test.ts` - API route tests
- `__tests__/api/tags-bulk-delete.test.ts`
- `__tests__/api/tags-delete.test.ts`
- `__tests__/api/tags-get-patch.test.ts`
- `__tests__/api/tags-merge.test.ts`

All mocks and assertions updated to handle new `TagOperationResult`
structure.

## Example User Experience

### Before
User merges 85 tags → 84 succeed, 1 fails → ❌ "Failed to merge tags"
(generic error, no details)

### After
User merges 85 tags → 84 succeed, 1 fails → ⚠️ Warning toast: "Merged 84
of 85 books. 1 failed - see details in modal"

Modal shows:
- ✅ Successfully merged 84 of 85 books into "target-tag"
- 📋 Failed Books (1) - expandable list showing:
  - Book title: "Example Book"
  - Calibre ID: 123
  - Error: "Calibre database locked"
- 📋 Copy Error Report button

## Testing
- ✅ All 1945 tests passing
- ✅ Build succeeds
- ✅ TypeScript compilation successful
- Manual testing recommended for UX validation

## Files Changed
- 21 files changed
- 1328 insertions, 387 deletions

## Breaking Changes
None - API endpoints maintain backward compatibility by still returning
error responses for complete failures.
…ons (#199)

## Summary
Adds 14 comprehensive test cases to cover partial success scenarios for
tag operations that were implemented in PR #197.

## What This PR Adds

This PR adds thorough test coverage for the partial success
functionality implemented in #197, specifically testing the critical
behavior that **Tome DB updates ALL books even when some Calibre syncs
fail**.

### Service Layer Tests (8 new tests in `tag.service.test.ts`)
For each operation (rename, delete, merge, bulkDelete), added:
1. **Partial failure handling test**: Verifies operations continue and
Tome DB is updated for ALL books even when some Calibre updates fail
2. **Failure enrichment test**: Verifies `calibreFailures` array
contains enriched data (calibreId, bookId, title, error)

### API Layer Tests (4 new tests across route files)
For each endpoint, added:
- Tests HTTP 200 responses with `partialSuccess: true`
- Verifies correct `successCount`, `failureCount`, `totalBooks`
- Verifies `calibreFailures` array exists with enriched data
- Confirms ALL books in Tome DB were updated

Test files updated:
- `__tests__/api/tags-get-patch.test.ts` (PATCH rename)
- `__tests__/api/tags-delete.test.ts` (DELETE)
- `__tests__/api/tags-merge.test.ts` (POST merge)
- `__tests__/api/tags-bulk-delete.test.ts` (POST bulk-delete)
- `__tests__/services/tag.service.test.ts` (service layer)

## Key Testing Improvements
- ✅ Fixes TypeScript type annotations for mock failures arrays
- ✅ Uses `mockReturnValueOnce()` to avoid cross-test interference
- ✅ Verifies both result structure AND database state
- ✅ Uses distinct book titles to verify enrichment works correctly

## Test Results
```
1957 tests pass
0 failures
```

## Why This Matters
While PR #197 implemented the partial success functionality and updated
existing tests to handle the new return types, it lacked explicit test
cases for **partial success scenarios** (when some Calibre updates
succeed and others fail). This PR ensures that critical behavior is
properly tested:

- ✅ Operations don't throw errors on partial failures
- ✅ Tome DB is always updated, even for books that fail Calibre sync
- ✅ Failure information is properly enriched with book details
- ✅ API responses correctly indicate partial success

## Related
- Builds on #197 (already merged)
- Closes testing gaps identified after #197 implementation
…200)

## Summary

This PR enhances the tags page user experience with two key
improvements:

1. **Scroll position preservation** - Users maintain their position in
the tag list when performing operations (rename, delete, merge)
2. **View books in Select Multiple mode** - Users can click tags to view
their books while in checkbox mode, with clearly separated click areas

## Changes

### Scroll Position Preservation
- Add scroll position tracking to TagList component via forwardRef
- Add before/after refetch callbacks to useTagManagement and useTagBooks
hooks
- Connect scroll preservation in tags page using refs and callbacks
- Use requestAnimationFrame for smooth scroll restoration after DOM
updates

### Select Multiple Mode Improvements
- Separate checkbox click area from tag content click area
- Wrap checkbox in container with stopPropagation to prevent conflicts
- Move onClick handler to content area (tag name, icon, book count)
- Add conditional cursor styling based on checkbox mode
- Checkbox clicks only toggle selection, content clicks open tag detail

## User Experience Impact

**Scroll Preservation:**
- ✅ No more losing your place after tag operations
- ✅ Seamless workflow when working with tags further down the list
- ✅ Works across all tag operations (rename, delete, merge, bulk
operations)

**Select Multiple Mode:**
- ✅ Click checkbox to select tags for merge/delete
- ✅ Click tag content to view which books have that tag
- ✅ No accidental tag opening when trying to select
- ✅ Makes informed decisions about merging/deleting easier

## Technical Details

**Files changed:** 5 files  
**Lines added:** +125  
**Lines removed:** -34

- `app/tags/page.tsx` - Coordinate scroll restoration
- `components/TagManagement/TagList.tsx` - Scroll position tracking
- `components/TagManagement/TagItem.tsx` - Separated click areas
- `hooks/useTagBooks.ts` - Refetch callbacks
- `hooks/useTagManagement.ts` - Refetch callbacks

## Testing

- ✅ TypeScript compilation passes
- ✅ ESLint passes with no warnings
- ✅ Scroll position preserved across operations
- ✅ Click areas properly separated in checkbox mode
- ✅ Works on desktop and mobile layouts
…s modal (#201)

## Summary
- Add `allowBackdropClose` prop to BaseModal to prevent accidental
dismissal during editing
- Make log progress modal full-height on mobile for better screen
utilization
The delete button on book cards in the tags page was using hover states (opacity-0 group-hover:opacity-100) which don't work reliably on iPad and other touch devices. Changed to always visible with a color transition on interaction instead.
Mason Fox and others added 24 commits February 7, 2026 11:06
## Summary

- **Fix Stats/Goals count mismatch** caused by malformed dates (Unix
timestamps stored as TEXT) bypassing string-based date comparisons
- **Add GLOB date validation guards** (`GLOB
'[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'`) to all date-filtering
queries across repositories
- **Create shared `ReadingStatsService`** so Stats and Goals pages use
identical counting logic, eliminating future divergence
- **Fix duplicate React key warning** on Goals page when a book is
re-read multiple times in the same year

## Root Cause

Session 1283 had a Unix timestamp `841276800` (Aug 1996) stored as TEXT
in `completed_date`. The Stats page used `>=` string comparison
(`completed_date >= "2025-12-31"`), which incorrectly matched it because
`"8" > "2"` lexicographically. The Goals page used `strftime('%Y',
completed_date)` which returned NULL for non-date strings, correctly
excluding it — hence the count mismatch.

## Changes

### Repository layer
- **`reading-goals.repository.ts`** — Added GLOB guards to
`getBooksCompletedInYear()`, `getYearsWithCompletedBooks()`,
`getBooksCompletedByMonth()`, `getBooksByCompletionYear()`; added
`sessionId` to `getBooksByCompletionYear()` select
- **`session.repository.ts`** — Added `countCompletedByYear()` and
`countCompletedByYearMonth()` with GLOB + strftime + orphan exclusion;
deprecated `countCompletedAfterDate()`
- **`progress.repository.ts`** — Added `getPagesReadByYear()`,
`getPagesReadByYearMonth()`, `getPagesReadByDate()` with GLOB guards

### Service layer
- **`reading-stats.service.ts`** (new) — Shared `ReadingStatsService`
with `getOverview()` used by Stats API, plus convenience methods for
year/month counting

### API layer
- **`stats/overview/route.ts`** — Simplified from ~70 lines to ~10
lines, delegates to `readingStatsService.getOverview()`
- **`reading-goals/books/route.ts`** — Updated response type to include
`sessionId`

### Component layer
- **`CompletedBooksSection.tsx`** — Uses `sessionId` as React key
instead of `book.id` to prevent duplicate key warnings on re-reads

### Tests
- **19 new integration tests** covering year/month counting, malformed
date exclusion, orphan exclusion, and Stats/Goals consistency

### Documentation
- **Migration 0016** — Added comment explaining defense-in-depth
relationship with GLOB guards

## Test Results

All 3752 tests pass across 179 test files.

Closes #349 #356
## Summary

- Fixes the mobile bottomsheet remaining visible behind the "Book
Completed!" modal when logging progress to 100%

## Root Cause

Z-index stacking conflict: the `BottomSheet` renders at
`z-[100]`/`z-[101]` while `FinishBookModal` (via `BaseModal` +
`createPortal`) renders at `z-50`. When progress hit 100%, `onClose()`
was intentionally skipped to "let FinishBookModal appear" — but the
completion modal was actually rendered **behind** the bottomsheet.

## Fix

Call `onClose()` in `LogProgressModal.tsx` to dismiss the bottomsheet
before showing the completion modal, matching the desktop behavior where
`BaseModal` closes and `FinishBookModal` replaces it.

**One-line change** in `components/Modals/LogProgressModal.tsx:109`.

## Testing

All 3796 tests pass.
## Summary

- Fixes inconsistent progress input width when switching between "Page"
and "Percentage" modes in the Log Progress modal
- The progress input field and date column would shift widths due to
nested flex items inheriting content-based intrinsic minimum widths
(`min-width: auto`)
- Changes the date column from `flex-1` to a fixed `md:w-48` for better
proportions since it only needs to fit a date value

## Changes

- `components/BookDetail/BookProgress.tsx`:
- Add `min-w-0` to progress column and number inputs to override browser
intrinsic sizing
  - Add `shrink-0` to dropdown wrapper div to prevent it from collapsing
  - Change date column from `flex-1` to fixed `md:w-48 md:shrink-0`
## Summary

- Adds server-side validation to reject progress entries where
`currentPage` exceeds `book.totalPages`, preventing corrupted progress
data (e.g., logging 1111 pages for a 111-page book)
- Validation lives in `calculateProgressMetrics()` so both
`logProgress()` and `updateProgress()` paths are covered with a single
check
- API routes return HTTP 400 with a clear error message: *"Page X
exceeds the book's total of Y pages. Please check your input or update
the book's page count."*

## Changes

**Production code (3 files):**
- `lib/services/progress.service.ts` — Added bounds check in
`calculateProgressMetrics()`
- `app/api/books/[id]/progress/route.ts` — Catch "exceeds" errors → 400
response
- `app/api/books/[id]/progress/[progressId]/route.ts` — Catch "exceeds"
errors → 400 response

**Tests (3 files, 12 new tests):**
- `__tests__/integration/services/progress.service.test.ts` — 8 tests
covering log + update page bounds
- `__tests__/e2e/api/progress/progress.test.ts` — 3 E2E tests for POST
endpoint
- `__tests__/e2e/api/progress/progress-route-coverage.test.ts` — 1 E2E
test for PATCH endpoint

## Design Decisions

- **Single validation point:** `calculateProgressMetrics()` is called by
both create and update flows, so one check covers everything
- **Books without totalPages:** Validation is skipped when `totalPages`
is null/undefined (some Calibre books lack page counts)
- **Hard reject:** Throws an error rather than silently capping —
protects user data integrity per the project constitution

## Testing

All 3808 tests pass across 180 test files with zero regressions.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…#347)

## Summary

Consolidates ~300 lines of duplicate streak calculation logic from
`lib/streaks.ts` into `StreakService`, establishing it as the single
source of truth for all streak operations.

## Motivation

- **Eliminates technical debt** from obsolete Bun workaround (Bun was
removed in PR #230 on Jan 6, 2026)
- **Fixes timezone bug divergence** between duplicate implementations
- **Prevents future bugs** from maintaining logic in two places
- **Aligns with project constitution** (DRY principle, service layer
pattern)

## Changes

### Code Changes
- ✅ Added `getActivityCalendar()` method to StreakService for feature
parity
- ✅ Fixed critical timezone conversion bug in
`StreakService.updateStreaks()`
- ✅ Migrated 4 production files to use StreakService:
  - `app/api/streaks/route.ts`
  - `app/api/stats/activity/route.ts`
  - `lib/services/session.service.ts`
  - `lib/db/seeders/index.ts`
- ✅ Deleted `lib/streaks.ts` (400 lines eliminated)
- ✅ Removed obsolete Bun workaround documentation
- ✅ Fixed 2 dynamic imports in StreakService

### Test Changes
- ✅ Updated test files to remove `lib/streaks` references
- ✅ Removed obsolete mocks for deleted module
- ✅ Simplified tests to use real StreakService implementation

### Documentation Changes
- ✅ Updated `docs/ARCHITECTURE.md` (3 references)
- ✅ Updated `.specify/memory/patterns.md` (4 examples)

## File Statistics

- **11 files modified**
- **1 file deleted** (`lib/streaks.ts`)
- **Net change**: -455 lines (+119 / -574)

## Testing

- ✅ All **3731 tests passing** (no regressions)
- ✅ Clean build with zero errors
- ✅ Verified zero remaining imports to deleted module

## Bug Fixes Included

**Critical timezone bug in `StreakService.updateStreaks()`**:
- **Problem**: Was not converting timezone-aware dates back to UTC
before storing
- **Impact**: Could cause incorrect streak dates when threshold changes
mid-day
- **Fix**: Now properly converts via `fromZonedTime()` before database
storage
- **Lines**: `lib/services/streak.service.ts:288-320`

## Related Issues

- Fixes #346
- Depends on #345 (merged - timezone fix in lib/streaks.ts)

## Implementation Plan

Detailed 8-phase, 27-task implementation breakdown available at
`docs/plans/consolidate-streak-logic.md`

## Review Notes

This PR has **zero breaking changes** - all API contracts are preserved.
The consolidation is purely internal refactoring that eliminates
duplicate code and fixes a latent bug in the process.

---------

Co-authored-by: Mason Fox <masonfox@pop-os.lan>
Co-authored-by: Mason Fox <masonfox@pop-os.tail3f4b5.ts.net>
…rvice

lib/streaks.ts is being removed in PR #368 and replaced by the new
StreakService singleton. This commit updates all test files that were
still mocking the old module to use the new service layer path.

Changes:
- Update 6 test files to mock @/lib/services/streak.service
- Update mock structure to match streakService singleton pattern
- All tests verified passing after changes

This prevents test failures when lib/streaks.ts is deleted.

Resolves module resolution error identified by Copilot code review.
The test suite was renamed to accurately reflect that it now tests
StreakService rather than the legacy lib/streaks.ts module. This
keeps test output and search results accurate.

Resolves Copilot feedback about outdated suite naming.
## Summary

Fixes a critical bug where the SQLite AUTOINCREMENT sequence was leaking
during Calibre sync operations, causing the sequence to advance
unnecessarily on UPDATE operations.

### Root Cause

Drizzle ORM's `onConflictDoUpdate()` causes SQLite to increment the
AUTOINCREMENT sequence even when performing updates (not just inserts).
The query attempts an INSERT first (incrementing sequence), then
switches to UPDATE on conflict. The sequence increment is not rolled
back.

### Impact

- **Before fix**: Sequence advanced by ~851 IDs on every sync cycle
(even for updates)
- **Production database**: 894 books with IDs 1-636,267, but sequence at
645,512
- **ID waste**: 99.86% waste (644,618 leaked IDs out of 645,512 total)

### Solution

**Refactored to split INSERT/UPDATE operations for better
architecture:**

1. **New `bulkInsert()` method** - Only performs INSERT operations
   - Sequence advances once per book (correct behavior)
   
2. **New `bulkUpdate()` method** - Only performs UPDATE operations  
   - Sequence does NOT advance (correct behavior)
   
3. **Removed `bulkUpsert()`** - No longer used anywhere
   - Had performance issues (850+ redundant existence checks per sync)
   - Caller already knew which books exist via `findAllByCalibreIds()`
   - Clean removal with no breaking changes

4. **Updated sync service** to call `bulkInsert()` and `bulkUpdate()`
separately
   - Eliminates ~850 redundant queries per sync
   - Uses existing `existingBooksMap` from line 200 (no extra queries)
- Clear separation of concerns (sync service orchestrates, repository
executes)

### Architectural Benefits

✅ **Separation of concerns** - Sync service decides insert vs update,
repository executes
✅ **No redundant work** - Eliminates 850+ redundant existence checks per
sync
✅ **Single responsibility** - Each method does one thing well  
✅ **Self-documenting** - Clear intent: `bulkInsert()` vs `bulkUpdate()`
✅ **Performance** - Fewer database queries overall  
✅ **Cleaner codebase** - Removed unused, problematic method

### Testing

**Fresh database (before fix):**
- Sync 1: sequence = 851 ✅
- Sync 2: sequence = 1,702 (+851) ❌ Bug!
- Sync 3: sequence = 2,553 (+851) ❌ Bug!

**Fresh database (after fix):**
- 5 consecutive syncs: sequence stable at 851 ✅
- 0% waste maintained across all syncs ✅

**Test suite:**
- Created 5 regression tests in `book-repository-autoincrement.test.ts`
  - `bulkInsert()` advances sequence correctly
  - `bulkUpdate()` does NOT advance sequence (critical check)
  - Multiple update cycles never leak
  - Mixed insert/update operations
  - Production scenario: 851 books regression test
- Fixed test isolation using unique `calibreId` ranges per test
- All 3,822 tests passing ✅

### Design Decision: No Migration

We decided **not** to include a companion migration to reset the
sequence on existing databases because:
- The gap between MAX(id) and sequence is harmless (SQLite supports 9.2
quintillion IDs)
- Avoiding migration complexity reduces maintenance burden and risk
- The bug fix itself prevents future leaks, which is what matters

Existing databases will have a one-time jump in the sequence (e.g.,
636,267 → 645,513) on the next sync, then the sequence will stay aligned
with MAX(id) going forward.

### Files Changed

- `lib/repositories/book.repository.ts` - Added `bulkInsert()`,
`bulkUpdate()`, removed `bulkUpsert()`
- `lib/sync-service.ts` - Split insert/update operations, eliminated
redundant queries
-
`__tests__/integration/repositories/books/book-repository-autoincrement.test.ts`
- 5 regression tests

### Before & After

**Before (fresh database):**
```
Sync 1: 851 books → sequence = 851
Sync 2: 851 books → sequence = 1,702 (leak!)
Sync 3: 851 books → sequence = 2,553 (leak!)
```

**After (fresh database):**
```
Sync 1: 851 books → sequence = 851
Sync 2: 851 books → sequence = 851 (stable)
Sync 3: 851 books → sequence = 851 (stable)
```
@codecov
Copy link

codecov bot commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 86.66667% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
lib/repositories/book.repository.ts 77.77% 2 Missing and 2 partials ⚠️

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #371      +/-   ##
==========================================
- Coverage   78.07%   78.00%   -0.07%     
==========================================
  Files         162      162              
  Lines        7289     7307      +18     
  Branches     1759     1763       +4     
==========================================
+ Hits         5691     5700       +9     
- Misses       1132     1139       +7     
- Partials      466      468       +2     
Flag Coverage Δ
unittests 78.00% <86.66%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
lib/sync-service.ts 94.85% <100.00%> (+0.15%) ⬆️
package.json 100.00% <100.00%> (ø)
lib/repositories/book.repository.ts 63.60% <77.77%> (+0.36%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Release bump to v0.6.4, centered on fixing SQLite AUTOINCREMENT sequence leakage during Calibre sync by splitting upsert behavior into explicit bulk insert vs bulk update paths.

Changes:

  • Bumped package.json version from 0.6.3 to 0.6.4.
  • Refactored sync to separate INSERT and UPDATE operations (bulkInsert() / bulkUpdate()) to prevent AUTOINCREMENT sequence leaks.
  • Added integration regression tests verifying the AUTOINCREMENT sequence remains stable across update-only sync cycles.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
package.json Version bump to 0.6.4 for release.
lib/sync-service.ts Splits book persistence into bulk insert + bulk update to avoid AUTOINCREMENT leaks.
lib/repositories/book.repository.ts Replaces upsert behavior with bulkInsert() and bulkUpdate() to avoid sequence increments on updates.
tests/integration/repositories/books/book-repository-autoincrement.test.ts Adds regression coverage for sequence stability across update cycles.
.opencode/command/semver.md Removes the semver command doc (file deletion).

- Add proper teardownTestDatabase() call in afterAll hook to prevent resource leaks
- Make beforeAll, beforeEach, and afterAll hooks async with proper await
- Import teardownTestDatabase and add missing beforeAll/afterAll imports
- Follows standard pattern from other integration tests

Addresses GitHub Copilot feedback on PR #371:
- r2813552750: Missing teardown prevents SQLite handles from closing
- r2813552725: Missing await on async setup/clear causes test flakiness
@masonfox masonfox merged commit 0d04148 into main Feb 16, 2026
6 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

Comments