Files
bookhoard/EBOOK_REFACTOR_PLAN.md
T
john-okeefe 2258bb2f5d docs(ebook): add comprehensive ebook.go refactor plan
- Add EBOOK_REFACTOR_PLAN.md with 856 lines of detailed instructions
- Split 1,350-line ebook.go into focused single-responsibility files
- Zero API changes, only code organization for maintainability
- Phase-by-phase safety checkpoints and rollback procedures

Target file organization after refactor:
- media.go (~600 lines): Media CRUD + metadata
- search.go (~80 lines): Query and search operations
- matching.go (~200 lines): Book matching and sync operations
- ebook.go (~150 lines): SetupRoutes only

Plan ensures AI can implement without breaking any functionality.
2026-02-07 00:16:20 -05:00

26 KiB

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)

// 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:

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:

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:

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

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:

// 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

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

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):

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):

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):

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:

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)

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)

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

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):

authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
libraryHandler := handlers.NewLibraryHandler(queries)
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
// ... other handlers

Add new handler instances:

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):

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:

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):

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:

// 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:

# 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:

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:

# 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:

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.