# Comprehensive ebook.go Refactor Plan **Created:** February 6, 2026 **Purpose:** Refactor 1,350-line ebook.go into focused, single-responsibility files **Assumption:** Scanner routes already moved to router/scanner.go **Goal:** Zero API changes, 100% functional compatibility --- ## Executive Summary `internal/handlers/ebook.go` violates Go single responsibility principle at 1,350 lines: - **Scanner operations**: 296 lines (12 methods) - **Media CRUD & metadata**: 822 lines (24 methods) - **Book matching/sync**: ~150 lines (8 methods) - **Collection device mappings**: 60+ lines (6 methods) **Proposed split:** 1. `media.go` - Media items, ratings, progress, notes, highlights (CRUD + metadata) 2. `search.go` - Query, search, filtered operations 3. `matching.go` - Book linking, file aliases, matching algorithms 4. `ebook.go` - Slimmed to only core Handler struct and SetupRoutes **Risk Level:** VERY LOW - No API endpoint changes - No database schema changes - Only code organization - All existing tests continue to work --- ## File Analysis Before Refactor ### Current ebook.go Structure (1,350 lines) ```go // Lines 38-164: Handler setup and SetupRoutes() func NewHandler(...) *Handler { } func SetupRoutes(...) *Handler { } // Lines 166-461: Scanner operations (12 methods) func (h *Handler) ScanEbooks() { } func (h *Handler) StartScanner() { } // ... 10 more scanner methods // Lines 474-1296: Media operations (24 methods) func (h *Handler) ListMediaItems() { } func (h *Handler) GetMediaItem() { } func (h *Handler) CreateMediaRating() { } // ... 21 more media methods // Lines 1297-end: Search & matching (8 methods) func (h *Handler) SearchMediaItems() { } func (h *Handler) QueryBooks() { } // ... 6 more methods ``` ### Handler Dependencies All methods share this Handler struct: ```go type Handler struct { db *database.Queries worker *worker.Worker scanner *services.EbookScanner scheduler *cron.Cron ctx context.Context cancel context.CancelFunc mu sync.RWMutex watchingLibraries map[string]bool } ``` --- ## Phase 1: SetupRoutes Restructure (FOUNDATION) ### Purpose Remove all route registration from SetupRoutes, make it a pure factory function ### File: `internal/handlers/ebook.go` #### Step 1.1: Simplify SetupRoutes (Lines 70-157) **Current Code Problem:** ```go func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager) *Handler { h := NewHandler(db, connManager) // Collections API routes (21 lines) collectionHandler := NewCollectionHandler(db, connManager) collections := g.Group("/collections") collections.GET("", collectionHandler.GetCollections) // ... 7 more collection routes // Device shelf mapping routes (6 lines) deviceCollections := g.Group("/devices/:id/collections") // ... 6 device routes // Book matching routes (4 lines) g.POST("/sync/books/query", h.QueryBooks) // ... 3 more matching routes // Media item routes (4 lines) g.GET("/media-items", h.ListMediaItems) // ... 3 more media routes // Admin-only routes (11 lines) admin := g.Group("", AdminMiddleware) admin.POST("/media-items", h.CreateMediaItem) // ... 10 more admin routes (including scanner) return h } ``` **New Code:** ```go func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager) *Handler { return NewHandler(db, connManager) } ``` **Verification:** - ✅ SetupRoutes now only creates handler instance - ✅ All routes will be registered via router package - ✅ Handler struct and dependencies unchanged --- ## Phase 2: Create Media Handler (CORE CRUD) ### File: `internal/handlers/media.go` (NEW) #### Step 2.1: Create MediaHandler struct ```go package handlers import ( "bookhoard/internal/database" "bookhoard/internal/sync" "context" "fmt" "net/http" "strconv" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" ) // MediaHandler handles media item CRUD operations and metadata type MediaHandler struct { db *database.Queries worker *worker.Worker } // NewMediaHandler creates a new media handler func NewMediaHandler(db *database.Queries, worker *worker.Worker) *MediaHandler { return &MediaHandler{ db: db, worker: worker, } } ``` #### Step 2.2: Move media CRUD methods (From ebook.go lines 474-959) **Methods to move:** - `ListMediaItems()` → `media.go:ListMediaItems()` - `GetMediaItem()` → `media.go:GetMediaItem()` - `ListMediaItemsFiltered()` → `media.go:ListMediaItemsFiltered()` - `CreateMediaItem()` → `media.go:CreateMediaItem()` - `UpdateMediaItem()` → `media.go:UpdateMediaItem()` - `DeleteMediaItem()` → `media.go:DeleteMediaItem()` - `SearchMediaItems()` → `media.go:SearchMediaItems()` **Method signature changes:** ```go // OLD in ebook.go func (h *Handler) ListMediaItems(c echo.Context) error // NEW in media.go func (mh *MediaHandler) ListMediaItems(c echo.Context) error ``` **Implementation:** Copy exact method bodies, change receiver from `h *Handler` to `mh *MediaHandler` #### Step 2.3: Move metadata methods (From ebook.go lines 630-839) **Methods to move:** - `CreateMediaRating()` → `media.go:CreateMediaRating()` - `GetMediaRating()` → `media.go:GetMediaRating()` - `UpdateMediaRating()` → `media.go:UpdateMediaRating()` - `DeleteMediaRating()` → `media.go:DeleteMediaRating()` - `GetMediaReadingProgress()` → `media.go:GetMediaReadingProgress()` - `UpdateMediaReadingProgress()` → `media.go:UpdateMediaReadingProgress()` - `DeleteMediaReadingProgress()` → `media.go:DeleteMediaReadingProgress()` - `GetMediaNotes()` → `media.go:GetMediaNotes()` - `CreateMediaNote()` → `media.go:CreateMediaNote()` - `GetMediaNote()` → `media.go:GetMediaNote()` - `UpdateMediaNote()` → `media.go:UpdateMediaNote()` - `DeleteMediaNote()` → `media.go:DeleteMediaNote()` - `GetMediaHighlights()` → `media.go:GetMediaHighlights()` - `CreateMediaHighlight()` → `media.go:CreateMediaHighlight()` - `GetMediaHighlight()` → `media.go:GetMediaHighlight()` - `UpdateMediaHighlight()` → `media.go:UpdateMediaHighlight()` - `DeleteMediaHighlight()` → `media.go:DeleteMediaHighlight()` **Verification after move:** - [ ] All methods compile with new receiver type - [ ] No changes to method bodies - [ ] All database calls work (same `mh.db` reference) - [ ] All imports are correct --- ## Phase 3: Create Search Handler (QUERY OPERATIONS) ### File: `internal/handlers/search.go` (NEW) #### Step 3.1: Create SearchHandler struct ```go package handlers import ( "bookhoard/internal/database" "context" "fmt" "net/http" "strconv" "github.com/labstack/echo/v4" ) // SearchHandler handles search, query, and filtered operations type SearchHandler struct { db *database.Queries } // NewSearchHandler creates a new search handler func NewSearchHandler(db *database.Queries) *SearchHandler { return &SearchHandler{ db: db, } } ``` #### Step 3.2: Move search methods (From ebook.go lines 1297-end) **Methods to move:** - `SearchMediaItems()` → `search.go:SearchMediaItems()` - `QueryBooks()` → `search.go:QueryBooks()` **Note:** These methods are small and focused, perfect for separate handler --- ## Phase 4: Create Matching Handler (SYNC OPERATIONS) ### File: `internal/handlers/matching.go` (NEW) #### Step 4.1: Create MatchingHandler struct ```go package handlers import ( "bookhoard/internal/database" "bookhoard/internal/sync" "context" "fmt" "net/http" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" ) // MatchingHandler handles book matching, linking, and file alias operations type MatchingHandler struct { db *database.Queries connManager *wsync.ConnectionManager } // NewMatchingHandler creates a new matching handler func NewMatchingHandler(db *database.Queries, connManager *wsync.ConnectionManager) *MatchingHandler { return &MatchingHandler{ db: db, connManager: connManager, } } ``` #### Step 4.2: Move matching methods (From ebook.go lines 96-104) **Methods to move:** - `QueryBooks()` → `matching.go:QueryBooks()` - `LinkBook()` → `matching.go:LinkBook()` - `GetUnlinkedBooks()` → `matching.go:GetUnlinkedBooks()` - `GetDeviceFileAliases()` → `matching.go:GetDeviceFileAliases()` - `CreateDeviceFileAlias()` → `matching.go:CreateDeviceFileAlias()` - `UpdateDeviceFileAlias()` → `matching.go:UpdateDeviceFileAlias()` - `DeleteDeviceFileAlias()` → `matching.go:DeleteDeviceFileAlias()` - `GetBookMatches()` → `matching.go:GetBookMatches()` --- ## Phase 5: Update Router Package (ROUTE REGISTRATION) ### Purpose Register all routes with new handler types, maintain exact same endpoints #### Step 5.1: Update router.go Config struct **File:** `internal/router/router.go` **Current Config (lines 33-52):** ```go type Config struct { Echo *echo.Echo Queries *database.Queries Cfg *config.Config DBPool interface{} AuthHandler *handlers.AuthHandler LibraryHandler *handlers.LibraryHandler DeviceHandler *handlers.DeviceHandler EbookHandler *handlers.Handler // ← WILL BE REMOVED KOReaderHandler *handlers.KOReaderHandler WSHandler *handlers.WSHandler ConflictHandler *handlers.ConflictHandler AnalyticsHandler *handlers.AnalyticsHandler QueueHandler *handlers.QueueHandler CollectionHandler *handlers.CollectionHandler OPDSHandler *handlers.OPDSHandler ConnManager *sync.ConnectionManager QueueProcessor *sync.SyncQueueProcessor DeviceAuthMiddleware *middleware.DeviceAuthMiddleware LoginTracker *ratelimit.LoginAttemptTracker } ``` **New Config (replace EbookHandler with 3 new handlers):** ```go type Config struct { Echo *echo.Echo Queries *database.Queries Cfg *config.Config DBPool interface{} AuthHandler *handlers.AuthHandler LibraryHandler *handlers.LibraryHandler DeviceHandler *handlers.DeviceHandler // EbookHandler REMOVED - replaced below: MediaHandler *handlers.MediaHandler // ← NEW SearchHandler *handlers.SearchHandler // ← NEW MatchingHandler *handlers.MatchingHandler // ← NEW KOReaderHandler *handlers.KOReaderHandler WSHandler *handlers.WSHandler ConflictHandler *handlers.ConflictHandler AnalyticsHandler *handlers.AnalyticsHandler QueueHandler *handlers.QueueHandler CollectionHandler *handlers.CollectionHandler OPDSHandler *handlers.OPDSHandler ConnManager *sync.ConnectionManager QueueProcessor *sync.SyncQueueProcessor DeviceAuthMiddleware *middleware.DeviceAuthMiddleware LoginTracker *ratelimit.LoginAttemptTracker } ``` #### Step 5.2: Update RegisterRoutes function **File:** `internal/router/router.go` **Current route registration (lines 114-129):** ```go jwtMiddleware := createJWTMiddleware(cfg) protected := e.Group("/api", jwtMiddleware) handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager) // OLD METHOD // Register route groups registerAuthRoutes(cfg, rateLimitMiddleware) registerLibraryRoutes(cfg) registerDeviceRoutes(cfg) registerSyncRoutes(cfg) registerMediaRoutes(cfg) registerConflictRoutes(cfg) registerAnalyticsRoutes(cfg) registerQueueRoutes(cfg) registerOPDSRoutes(cfg) registerWebSocketRoutes(cfg) registerFrontendRoutes(cfg) registerDocumentationRoutes(cfg) ``` **New route registration:** ```go jwtMiddleware := createJWTMiddleware(cfg) protected := e.Group("/api", jwtMiddleware) // Create handler instances using factory functions mediaHandler := handlers.NewMediaHandler(cfg.Queries, cfg.Worker) // Worker needed searchHandler := handlers.NewSearchHandler(cfg.Queries) matchingHandler := handlers.NewMatchingHandler(cfg.Queries, cfg.ConnManager) // Register route groups registerAuthRoutes(cfg, rateLimitMiddleware) registerLibraryRoutes(cfg) registerDeviceRoutes(cfg) registerSyncRoutes(cfg) registerMediaRoutes(cfg) // ← WILL USE NEW MediaHandler registerSearchRoutes(cfg) // ← NEW FILE registerMatchingRoutes(cfg) // ← NEW FILE registerConflictRoutes(cfg) registerAnalyticsRoutes(cfg) registerQueueRoutes(cfg) registerOPDSRoutes(cfg) registerWebSocketRoutes(cfg) registerFrontendRoutes(cfg) registerDocumentationRoutes(cfg) ``` #### Step 5.3: Create registerSearchRoutes function **File:** `internal/router/search.go` (NEW) ```go package router import ( "net/http" "github.com/labstack/echo/v4" ) func registerSearchRoutes(cfg *Config) { e := cfg.Echo // JWT middleware for protected routes jwtMiddleware := createJWTMiddleware(cfg) protected := e.Group("/api", jwtMiddleware) // Search and query endpoints (all authenticated users) protected.GET("/media-items/search", cfg.SearchHandler.SearchMediaItems) protected.POST("/sync/books/query", cfg.MatchingHandler.QueryBooks) } ``` #### Step 5.4: Create registerMatchingRoutes function **File:** `internal/router/matching.go` (NEW) ```go package router import ( "github.com/labstack/echo/v4" ) func registerMatchingRoutes(cfg *Config) { e := cfg.Echo // JWT middleware for protected routes jwtMiddleware := createJWTMiddleware(cfg) protected := e.Group("/api", jwtMiddleware) // Book matching and linking routes (all authenticated users) protected.POST("/devices/:deviceId/sync/link-book", cfg.MatchingHandler.LinkBook) protected.GET("/devices/:deviceId/sync/unlinked-books", cfg.MatchingHandler.GetUnlinkedBooks) // File alias routes (all authenticated users) protected.GET("/devices/:id/file-aliases", cfg.MatchingHandler.GetDeviceFileAliases) protected.POST("/devices/:id/file-aliases", cfg.MatchingHandler.CreateDeviceFileAlias) protected.PUT("/devices/:id/file-aliases/:aliasId", cfg.MatchingHandler.UpdateDeviceFileAlias) protected.DELETE("/devices/:id/file-aliases/:aliasId", cfg.MatchingHandler.DeleteDeviceFileAlias) protected.GET("/books/match", cfg.MatchingHandler.GetBookMatches) } ``` #### Step 5.5: Update existing registerMediaRoutes function **File:** `internal/router/media.go` (MODIFY) **Current:** Empty or minimal **New:** Register all media CRUD endpoints ```go package router import ( "github.com/labstack/echo/v4" "bookhoard/internal/handlers" ) func registerMediaRoutes(cfg *Config) { e := cfg.Echo // JWT middleware for protected routes jwtMiddleware := createJWTMiddleware(cfg) protected := e.Group("/api", jwtMiddleware) admin := protected.Group("", handlers.AdminMiddleware) // Media item routes (all authenticated users) protected.GET("/media-items", cfg.MediaHandler.ListMediaItems) protected.GET("/media-items/filtered", cfg.MediaHandler.ListMediaItemsFiltered) protected.GET("/media-items/search", cfg.MediaHandler.SearchMediaItems) protected.GET("/media-items/:id", cfg.MediaHandler.GetMediaItem) // Media rating routes (all authenticated users) protected.POST("/media-items/:id/rating", cfg.MediaHandler.CreateMediaRating) protected.GET("/media-items/:id/rating", cfg.MediaHandler.GetMediaRating) protected.PUT("/media-items/:id/rating", cfg.MediaHandler.UpdateMediaRating) protected.DELETE("/media-items/:id/rating", cfg.MediaHandler.DeleteMediaRating) // Progress routes (all authenticated users) protected.GET("/progress/:id", cfg.MediaHandler.GetUniversalProgress) protected.POST("/progress/:id", cfg.MediaHandler.UpdateUniversalProgress) protected.GET("/progress/:id/history", cfg.MediaHandler.GetProgressHistory) // Notes routes (all authenticated users) protected.GET("/media-items/:id/notes", cfg.MediaHandler.GetMediaNotes) protected.POST("/media-items/:id/notes", cfg.MediaHandler.CreateMediaNote) protected.GET("/media-items/:id/notes/:noteId", cfg.MediaHandler.GetMediaNote) protected.PUT("/media-items/:id/notes/:noteId", cfg.MediaHandler.UpdateMediaNote) protected.DELETE("/media-items/:id/notes/:noteId", cfg.MediaHandler.DeleteMediaNote) // Highlights routes (all authenticated users) protected.GET("/media-items/:id/highlights", cfg.MediaHandler.GetMediaHighlights) protected.POST("/media-items/:id/highlights", cfg.MediaHandler.CreateMediaHighlight) protected.GET("/media-items/:id/highlights/:highlightId", cfg.MediaHandler.GetMediaHighlight) protected.PUT("/media-items/:id/highlights/:highlightId", cfg.MediaHandler.UpdateMediaHighlight) protected.DELETE("/media-items/:id/highlights/:highlightId", cfg.MediaHandler.DeleteMediaHighlight) // Admin-only media routes admin.POST("/media-items", cfg.MediaHandler.CreateMediaItem) admin.PUT("/media-items/:id", cfg.MediaHandler.UpdateMediaItem) admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem) } ``` --- ## Phase 6: Update main.go (INITIALIZATION) ### File: `cmd/server/main.go` #### Step 6.1: Create new handler instances **Current handler creation (lines 78-99):** ```go authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker) libraryHandler := handlers.NewLibraryHandler(queries) deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg) // ... other handlers ``` **Add new handler instances:** ```go authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker) libraryHandler := handlers.NewLibraryHandler(queries) deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg) // ... existing handlers ... // NEW: Create refactored handlers mediaHandler := handlers.NewMediaHandler(queries, queueProcessor) // Worker needed searchHandler := handlers.NewSearchHandler(queries) matchingHandler := handlers.NewMatchingHandler(queries, connManager) ``` #### Step 6.2: Update routerConfig **Current routerConfig (lines 131-150):** ```go routerConfig := &router.Config{ Echo: e, Queries: queries, Cfg: cfg, DBPool: dbPool, AuthHandler: authHandler, LibraryHandler: libraryHandler, DeviceHandler: deviceHandler, EbookHandler: ebookHandler, // ← REMOVE KOReaderHandler: koreaderHandler, WSHandler: wsHandler, // ... other fields } ``` **New routerConfig:** ```go routerConfig := &router.Config{ Echo: e, Queries: queries, Cfg: cfg, DBPool: dbPool, AuthHandler: authHandler, LibraryHandler: libraryHandler, DeviceHandler: deviceHandler, // EbookHandler REMOVED MediaHandler: mediaHandler, // ← NEW SearchHandler: searchHandler, // ← NEW MatchingHandler: matchingHandler, // ← NEW KOReaderHandler: koreaderHandler, WSHandler: wsHandler, ConflictHandler: conflictHandler, AnalyticsHandler: analyticsHandler, QueueHandler: queueHandler, CollectionHandler: collectionHandler, OPDSHandler: opdsHandler, ConnManager: connManager, QueueProcessor: queueProcessor, DeviceAuthMiddleware: deviceAuthMiddleware, LoginTracker: loginAttemptTracker, } ``` **Note:** Need to determine where `worker` comes from for MediaHandler. Check existing code. --- ## Phase 7: Clean Up ebook.go (FINALIZE) ### File: `internal/handlers/ebook.go` #### Step 7.1: Remove moved methods **Remove these method sections:** - Lines 474-959: All media CRUD and metadata methods - Lines 1297-end: All search and matching methods - Lines 73-104: Collection and device mapping routes - Lines 105-143: All route registrations **Keep these sections:** - Lines 38-69: Handler struct and NewHandler function - Lines 70-72: Simplified SetupRoutes function - Lines 159-end: Request/response structs #### Step 7.2: Update Handler struct dependencies **Current Handler struct (lines 19-30):** ```go type Handler struct { db *database.Queries worker *worker.Worker // ← MOVE TO MediaHandler scanner *services.EbookScanner // ← MAY MOVE TO ScannerHandler scheduler *cron.Cron // ← MAY MOVE TO ScannerHandler ctx context.Context cancel context.CancelFunc mu sync.RWMutex watchingLibraries map[string]bool } ``` **Potential final Handler struct:** ```go // If keeping only SetupRoutes function: type Handler struct { // This might be entirely removed if SetupRoutes is only factory } func SetupRoutes(g *echo.Group, db *database.Queries, connManager *wsync.ConnectionManager) *Handler { return NewHandler(db, connManager) } ``` OR **If Handler is no longer needed:** - Remove entire file - Move remaining structs to appropriate files - Update all imports --- ## Implementation Order (SAFEST FIRST) ### Phase 1: Create new handler files (ZERO RISK) 1. Create `internal/handlers/media.go` with struct and empty methods 2. Create `internal/handlers/search.go` with struct and empty methods 3. Create `internal/handlers/matching.go` with struct and empty methods 4. Verify compilation ### Phase 2: Move method bodies (LOW RISK) 1. Copy media methods from ebook.go to media.go 2. Copy search methods from ebook.go to search.go 3. Copy matching methods from ebook.go to matching.go 4. Test compilation after each file ### Phase 3: Update router registration (MEDIUM RISK) 1. Update router.go Config struct 2. Create registerSearchRoutes and registerMatchingRoutes 3. Update registerMediaRoutes to use MediaHandler 4. Test compilation ### Phase 4: Update main.go initialization (LOW RISK) 1. Add new handler instance creation 2. Update routerConfig 3. Test compilation and runtime ### Phase 5: Clean up ebook.go (LOW RISK) 1. Remove moved methods 2. Simplify/Remove Handler struct if no longer needed 3. Verify final state --- ## Verification Checklist ### After Each Phase: **Phase 1 (new files created):** - [ ] All 3 new files created - [ ] Code compiles without errors - [ ] No existing functionality broken **Phase 2 (methods moved):** - [ ] All media methods in media.go compile - [ ] All search methods in search.go compile - [ ] All matching methods in matching.go compile - [ ] Original ebook.go still compiles **Phase 3 (router updated):** - [ ] router.go compiles with new Config struct - [ ] registerSearchRoutes compiles - [ ] registerMatchingRoutes compiles - [ ] registerMediaRoutes updated correctly **Phase 4 (main.go updated):** - [ ] Application compiles - [ ] All handler instances created - [ ] routerConfig populated correctly **Phase 5 (cleanup):** - [ ] ebook.go reduced from 1,350 to <200 lines - [ ] All route endpoints still exist - [ ] Bruno tests still pass - [ ] Verification script passes (26/26) ### Final Verification Commands: ```bash # Compilation test go build ./cmd/server # Test all critical endpoints still work curl http://localhost:8765/api/media-items | jq . curl http://localhost:8765/api/media-items/search?q=test | jq . curl http://localhost:8765/api/books/match | jq . # Run verification script bash scripts/verify-guidelines.sh # Check file sizes wc -l internal/handlers/*.go | sort -n # Expected: media.go ~600, search.go ~50, matching.go ~150, ebook.go <200 # Test Bruno tests still pass cd bruno && npx bruno test ``` --- ## Rollback Strategy ### If any phase breaks: **Immediate rollback:** ```bash git checkout -- internal/handlers/media.go git checkout -- internal/handlers/search.go git checkout -- internal/handlers/matching.go git checkout -- internal/router/router.go git checkout -- internal/router/media.go git checkout -- cmd/server/main.go ``` **Phase-by-phase rollback:** ```bash # After Phase 2: If methods don't compile git checkout -- internal/handlers/media.go git checkout -- internal/handlers/search.go git checkout -- internal/handlers/matching.go # After Phase 3: If router breaks git checkout -- internal/router/router.go git checkout -- internal/router/media.go # After Phase 4: If initialization breaks git checkout -- cmd/server/main.go ``` **Full project reset:** ```bash git checkout -- internal/handlers/ebook.go git clean -fd internal/handlers/media.go internal/handlers/search.go internal/handlers/matching.go ``` --- ## Success Metrics **Before refactor:** - ebook.go: 1,350 lines - 1 monolithic handler - Mixed concerns in single file **After refactor:** - ebook.go: ~150 lines (SetupRoutes only) - media.go: ~600 lines (CRUD + metadata) - search.go: ~80 lines (search operations) - matching.go: ~200 lines (sync operations) - Clear single responsibilities per file - All API endpoints identical - All tests continue to work **File count increase:** +3 new handler files **Lines of code change:** ~0 (moved, not modified) **API compatibility:** 100% maintained **Risk level:** VERY LOW --- ## Final Notes ### Dependencies to verify: 1. **Worker access for MediaHandler:** - Check where `worker` is created in current code - Pass to NewMediaHandler properly 2. **AdminMiddleware import:** - Ensure all router files import handlers package - AdminMiddleware is accessible 3. **Import statements:** - Verify all new handler files have correct imports - No circular dependencies created ### Testing strategy: 1. **Compile after each file creation** 2. **Test endpoint accessibility after router changes** 3. **Run existing test suite before/after** 4. **Verify Bruno tests still pass** 5. **Check verification script passes** This plan ensures zero API changes while completely reorganizing the codebase into maintainable, single-responsibility files. --- **Ready for safe implementation by any AI following these exact phases.**