From 4b68ef8abdd5514f24c402a7d0e5d189a89d73ad Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Fri, 6 Feb 2026 21:59:30 -0500 Subject: [PATCH] docs(scanner): add comprehensive scanner restoration and enhancement plan - Document missing 10 scanner endpoints lost during router refactor - Plan for library-type-aware scanner implementation - Application lifecycle management via App pattern - Background services auto-start (scheduler, watch mode) - Graceful shutdown with signal handling - Complete implementation guide with code snippets and testing checklist - Safe phased approach with rollback procedures Plan includes: - Phase 1: Create scanner routes file (internal/router/scanner.go) - Phase 2: Update router to capture EbookHandler - Phase 3: Implement library-type-aware scanning - Phase 4: Create application lifecycle management (internal/app/app.go) - Phase 5: Update main.go to use App pattern - Phase 6: Testing and verification Ready for implementation in next session. --- SCANNER_RESTORATION_PLAN.md | 1116 +++++++++++++++++++++++++++++++++++ 1 file changed, 1116 insertions(+) create mode 100644 SCANNER_RESTORATION_PLAN.md diff --git a/SCANNER_RESTORATION_PLAN.md b/SCANNER_RESTORATION_PLAN.md new file mode 100644 index 0000000..7740cc7 --- /dev/null +++ b/SCANNER_RESTORATION_PLAN.md @@ -0,0 +1,1116 @@ +# Scanner System Restoration & Enhancement Plan + +**Created:** February 6, 2026 +**Context:** Restore missing scanner functionality lost during router refactor, implement library-type-aware scanning +**Status:** Ready to implement + +--- + +## Executive Summary + +This plan restores the complete scanner system that was broken during the router refactor (commit 9bc8cd7), plus adds library-type-awareness to prevent cross-contamination between library types. + +**Changes:** +- 10 scanner endpoints restored +- Library-type-aware scanner implementation +- Background services auto-start (scheduler, watch mode) +- Graceful shutdown with signal handling +- Application lifecycle management via App struct + +**Files to Create:** +1. `internal/router/scanner.go` (new) +2. `internal/app/app.go` (new) + +**Files to Modify:** +1. `internal/router/router.go` (add scanner route registration) +2. `cmd/server/main.go` (use App pattern) +3. `internal/services/ebook_scanner.go` (library-type-aware scanning) + +**Risk Assessment:** LOW - All changes are additive or wrap existing code + +--- + +## Phase 1: Create Scanner Routes File + +### File: `internal/router/scanner.go` (NEW) + +**Purpose:** Register all 10 scanner endpoints + +**Dependencies:** +- `cfg.Config` struct must have `EbookHandler` field +- `EbookHandler` is returned from `handlers.SetupRoutes()` + +**Complete Code:** + +```go +package router + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +// registerScannerRoutes registers all scanner-related endpoints +func registerScannerRoutes(cfg *Config) { + e := cfg.Echo + + // JWT middleware for protected routes + jwtMiddleware := createJWTMiddleware(cfg) + protected := e.Group("/api", jwtMiddleware) + + // User scan settings endpoints (existing in auth.go) + protected.GET("/library/scan-settings", cfg.AuthHandler.GetScanSettings) + protected.PUT("/library/scan-settings", cfg.AuthHandler.UpdateScanSettings) + + // Scanner control endpoints (ebook.go Handler) + scanner := protected.Group("/scanner") + scanner.POST("/scan", cfg.EbookHandler.ScanEbooks) + scanner.GET("/status/:jobId", cfg.EbookHandler.GetScanStatus) + scanner.POST("/start", cfg.EbookHandler.StartScanner) + scanner.POST("/stop", cfg.EbookHandler.StopScanner) + + // Watch mode endpoints + watch := protected.Group("/scanner/watch") + watch.POST("/start", cfg.EbookHandler.StartWatchMode) + watch.POST("/stop", cfg.EbookHandler.StopWatchMode) + watch.GET("/status", cfg.EbookHandler.GetWatchModeStatus) +} +``` + +**Verification:** +- 10 endpoints registered +- All use cfg.EbookHandler methods +- All protected by JWT middleware + +--- + +## Phase 2: Update Router to Call Scanner Routes + +### File: `internal/router/router.go` + +**Change 1: Capture EbookHandler from SetupRoutes** + +**Location:** Line 113-114 + +**Current Code:** +```go +jwtMiddleware := createJWTMiddleware(cfg) +protected := e.Group("/api", jwtMiddleware) +handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager) + +// Register route groups +``` + +**New Code:** +```go +jwtMiddleware := createJWTMiddleware(cfg) +protected := e.Group("/api", jwtMiddleware) +ebookHandler := handlers.SetupRoutes(protected, cfg.Queries, cfg.ConnManager) + +// Register route groups +``` + +**Change 2: Add EbookHandler to Config struct** + +**Location:** Line 33-52 (Config struct definition) + +**Add to struct:** +```go +type Config struct { + Echo *echo.Echo + Queries *database.Queries + Cfg *config.Config + DBPool interface{} // pgxpool.Pool interface + AuthHandler *handlers.AuthHandler + LibraryHandler *handlers.LibraryHandler + DeviceHandler *handlers.DeviceHandler + EbookHandler *handlers.Handler // ← ADD THIS LINE + 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 +} +``` + +**Change 3: Add scanner route registration** + +**Location:** Line 124 (after registerAnalyticsRoutes) + +**Current Code:** +```go +registerAnalyticsRoutes(cfg) +registerQueueRoutes(cfg) +registerOPDSRoutes(cfg) +``` + +**New Code:** +```go +registerAnalyticsRoutes(cfg) +registerQueueRoutes(cfg) +registerScannerRoutes(cfg) // ← ADD THIS LINE +registerOPDSRoutes(cfg) +``` + +**Verification:** +- EbookHandler captured from SetupRoutes +- Added to Config struct +- registerScannerRoutes called in RegisterRoutes + +--- + +## Phase 3: Implement Library-Type-Aware Scanner + +### File: `internal/services/ebook_scanner.go` + +**Change 1: Add libraryTypes cache field** + +**Location:** Line 59-65 (EbookScanner struct) + +**Current Code:** +```go +type EbookScanner struct { + db *database.Queries + watcher *fsnotify.Watcher + folders []string + adminID pgtype.UUID + defaultLibraryID pgtype.UUID +} +``` + +**New Code:** +```go +type EbookScanner struct { + db *database.Queries + watcher *fsnotify.Watcher + folders []string + adminID pgtype.UUID + defaultLibraryID pgtype.UUID + libraryTypes map[string][]string // folder -> allowed extensions cache +} +``` + +**Change 2: Initialize libraryTypes in NewHandler** + +**Location:** Line 73-74 (in NewEbookScanner function) + +**Current Code:** +```go +func NewEbookScanner(db *database.Queries) *EbookScanner { + watcher, err := fsnotify.NewWatcher() + if err != nil { + panic(fmt.Sprintf("Failed to create file watcher: %v", err)) + } + + return &EbookScanner{ + db: db, + watcher: watcher, + folders: []string{}, + adminID: pgtype.UUID{}, + defaultLibraryID: pgtype.UUID{Valid: false}, + } +} +``` + +**New Code:** +```go +func NewEbookScanner(db *database.Queries) *EbookScanner { + watcher, err := fsnotify.NewWatcher() + if err != nil { + panic(fmt.Sprintf("Failed to create file watcher: %v", err)) + } + + return &EbookScanner{ + db: db, + watcher: watcher, + folders: []string{}, + adminID: pgtype.UUID{}, + defaultLibraryID: pgtype.UUID{Valid: false}, + libraryTypes: make(map[string][]string), // ← ADD THIS + } +} +``` + +**Change 3: Build library types cache in SetFolders** + +**Location:** Line 86-109 (SetFolders function) + +**Current Code:** +```go +func (s *EbookScanner) SetFolders(folders []string) error { + s.folders = folders + + // Remove old watch if exists + if s.watcher != nil { + s.watcher.Close() + } + + // Create new watcher + watcher, err := fsnotify.NewWatcher() + if err != nil { + return fmt.Errorf("failed to create watcher: %v", err) + } + s.watcher = watcher + + // Add all folders to watch + for _, folder := range folders { + if err := s.watcher.Add(folder); err != nil { + fmt.Printf("Warning: failed to watch folder %s: %v\n", folder, err) + } + } + + return nil +} +``` + +**New Code:** +```go +func (s *EbookScanner) SetFolders(folders []string) error { + s.folders = folders + + // Remove old watch if exists + if s.watcher != nil { + s.watcher.Close() + } + + // Create new watcher + watcher, err := fsnotify.NewWatcher() + if err != nil { + return fmt.Errorf("failed to create watcher: %v", err) + } + s.watcher = watcher + + // Build cache of allowed extensions per folder + s.libraryTypes = make(map[string][]string) + ctx := context.Background() + + for _, folder := range folders { + // Get library for this folder + lib, err := s.db.GetLibraryByFolder(ctx, folder) + if err != nil { + fmt.Printf("Warning: failed to get library for folder %s: %v\n", folder, err) + continue + } + + // Get library type with allowed extensions + libType, err := s.db.GetLibraryType(ctx, lib.LibraryTypeID) + if err != nil { + fmt.Printf("Warning: failed to get library type for %s: %v\n", folder, err) + continue + } + + // Cache allowed extensions for this folder + s.libraryTypes[folder] = libType.AllowedExtensions + fmt.Printf("Scanner: Folder %s allows extensions: %v\n", folder, libType.AllowedExtensions) + } + + // Add all folders to watch + for _, folder := range folders { + if err := s.watcher.Add(folder); err != nil { + fmt.Printf("Warning: failed to watch folder %s: %v\n", folder, err) + } + } + + return nil +} +``` + +**Change 4: Replace isEbookFile with isScannableFile** + +**Location:** Line 168-177 (isEbookFile function) + +**Current Code:** +```go +func (s *EbookScanner) isEbookFile(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + switch ext { + case ".epub", ".pdf", ".mobi", ".azw3", ".fb2", ".txt": + return true + default: + return false + } +} +``` + +**New Code:** +```go +// isScannableFile checks if a file should be scanned based on library type configuration +func (s *EbookScanner) isScannableFile(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + + // Find which folder this file belongs to + var folder string + for _, f := range s.folders { + if strings.HasPrefix(path, f) { + folder = f + break + } + } + + // If no folder match, don't scan + if folder == "" { + return false + } + + // Get allowed extensions for this folder's library + allowed, ok := s.libraryTypes[folder] + if !ok { + // No library type info, skip file + fmt.Printf("Warning: No library type info for folder %s, skipping %s\n", folder, path) + return false + } + + // Check if file extension is allowed for this library type + for _, allowedExt := range allowed { + if ext == strings.ToLower(allowedExt) { + return true + } + } + + return false +} +``` + +**Change 5: Update ScanFolders to use isScannableFile** + +**Location:** Line 146 (in ScanFolders function) + +**Current Code:** +```go +// Check if it's an ebook file +if s.isEbookFile(path) { +``` + +**New Code:** +```go +// Check if file should be scanned based on library type +if s.isScannableFile(path) { +``` + +**Verification:** +- libraryTypes field added to struct +- Initialized in NewEbookScanner +- Populated in SetFolders from database +- isScannableFile checks against library's allowed list +- Prevents cross-contamination between library types + +--- + +## Phase 4: Create Application Lifecycle Management + +### File: `internal/app/app.go` (NEW) + +**Purpose:** Manage application lifecycle, start/stop background services + +**Complete Code:** + +```go +package app + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v4" +) + +// App represents the application with all its components +type App struct { + Echo *echo.Echo + EbookHandler interface{} // *handlers.Handler from handlers/ebook.go + Config interface{} // *config.Config + DBPool interface{} // pgxpool.Pool + shutdownFuncs []func() error +} + +// New creates a new application instance +func New(echo *echo.Echo, ebookHandler interface{}, cfg interface{}, dbPool interface{}) *App { + return &App{ + Echo: echo, + EbookHandler: ebookHandler, + Config: cfg, + DBPool: dbPool, + shutdownFuncs: []func() error{}, + } +} + +// Start begins the application lifecycle +func (a *App) Start() error { + log.Println("Starting application...") + + // Start background services + if err := a.startBackgroundServices(); err != nil { + return fmt.Errorf("failed to start background services: %w", err) + } + + // Start HTTP server (blocking) + addr := a.Echo.Addr(":8080") // Will be overridden by Echo + if err := a.Echo.Start(addr); err != nil && err != http.ErrServerClosed { + return fmt.Errorf("failed to start server: %w", err) + } + + return nil +} + +// Stop gracefully shuts down the application +func (a *App) Stop() { + log.Println("Shutting down application...") + + // Stop background services in reverse order + for i := len(a.shutdownFuncs) - 1; i >= 0; i-- { + if fn := a.shutdownFuncs[i]; fn != nil { + if err := fn(); err != nil { + log.Printf("Error during shutdown: %v", err) + } + } + } + + // Give HTTP server time to finish in-flight requests + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + if err := a.Echo.Shutdown(ctx); err != nil { + log.Printf("Error during server shutdown: %v", err) + } + + log.Println("Application stopped") +} + +// startBackgroundServices initializes all background services +func (a *App) startBackgroundServices() error { + log.Println("Starting background services...") + + // Start scheduler for auto-scanning + // Note: EbookHandler has StartScheduler() method + if err := a.callHandlerMethod("StartScheduler"); err != nil { + log.Printf("Warning: Failed to start scheduler: %v", err) + } + + // Start watch mode for all libraries (with delay) + a.shutdownFuncs = append(a.shutdownFuncs, func() error { + // Stop watch mode and scheduler on shutdown + a.callHandlerMethod("StopScheduler") + return nil + }) + + // Start watch mode in background after delay + go func() { + time.Sleep(2 * time.Second) // Wait for server to be ready + if err := a.callHandlerMethod("StartWatchModeForAllLibraries", context.Background()); err != nil { + log.Printf("Warning: Failed to start watch mode: %v", err) + } + }() + + return nil +} + +// callHandlerMethod calls a method on the EbookHandler by name +func (a *App) callHandlerMethod(methodName string, args ...interface{}) error { + // Use reflection or type assertion to call methods + // For now, we'll need to type assert to access methods + // This is a simplified version - in production, use proper reflection + handler, ok := a.EbookHandler.(interface { + StartScheduler() + StopScheduler() + StartWatchModeForAllLibraries(ctx context.Context) error + }) + if !ok { + return fmt.Errorf("handler does not support method: %s", methodName) + } + + switch methodName { + case "StartScheduler": + handler.StartScheduler() + case "StopScheduler": + handler.StopScheduler() + case "StartWatchModeForAllLibraries": + if len(args) > 0 { + if ctx, ok := args[0].(context.Context); ok { + return handler.StartWatchModeForAllLibraries(ctx) + } + } + } + + return nil +} + +// WaitForShutdown blocks until a termination signal is received +func (a *App) WaitForShutdown() { + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) + + // Wait for signal + sig := <-sigChan + log.Printf("Received signal: %v", sig) + + // Initiate graceful shutdown + a.Stop() +} +``` + +**Note:** The app.go file uses type assertions to call Handler methods. We need to ensure the Handler type is properly exposed or we'll need to use reflection. An alternative is to define an interface. + +**Alternative: Define Service Interface** + +Add this to app.go before the App struct: + +```go +// ScannerService defines the interface for scanner background services +type ScannerService interface { + StartScheduler() + StopScheduler() + StartWatchModeForAllLibraries(ctx context.Context) error +} +``` + +Then change the App struct: + +```go +type App struct { + Echo *echo.Echo + ScannerService ScannerService // Use interface instead of interface{} + Config interface{} + DBPool interface{} + shutdownFuncs []func() error +} + +// In New(): +func New(echo *echo.Echo, scanner ScannerService, ...) *App { + return &App{ + ScannerService: scanner, + // ... + } +} +``` + +**Verification:** +- App struct created +- Start() calls background services +- Stop() performs graceful shutdown +- Signal handling implemented +- No existing functionality broken + +--- + +## Phase 5: Update main.go to Use App Pattern + +### File: `cmd/server/main.go` + +**Current Code (Lines 64-163):** +```go +func main() { + cfg := config.LoadConfig() + + dbPool, err := pgxpool.New(context.Background(), cfg.DatabaseURL()) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + defer dbPool.Close() + + queries := database.New(dbPool) + + // Create login attempt tracker: 5 failed attempts = 15 minute lockout + loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute) + + authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker) + libraryHandler := handlers.NewLibraryHandler(queries) + deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg) + deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries) + + // Create WebSocket connection manager + connManager := sync.NewConnectionManager() + connManager.StartCleanupTask() + + // Create sync queue processor + queueProcessor := sync.NewSyncQueueProcessor(queries) + go queueProcessor.Start(context.Background()) + + koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor) + wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware) + conflictHandler := handlers.NewConflictHandler(queries, connManager) + analyticsHandler := handlers.NewAnalyticsHandler(queries) + queueHandler := handlers.NewQueueHandler(queries, queueProcessor) + + // Create conversion service for EPUB→KEPUB conversion + conversionService := services.NewConversionService(queries, "/var/bookhoard/cache/kepub") + opdsHandler := handlers.NewOPDSHandler(queries, conversionService) + + e := echo.New() + + // Set up validator + v := validator.New() + + // Register custom password complexity validator + if err := ratelimit.RegisterPasswordValidation(v); err != nil { + log.Fatal("Failed to register password validator:", err) + } + e.Validator = &CustomValidator{validator: v} + + // Middleware + e.Use(echomiddleware.Logger()) + e.Use(echomiddleware.Recover()) + e.Use(echomiddleware.CORS()) + e.Use(ratelimit.RequestTracingMiddleware(cfg)) + + // ======================================================================== + // ROUTER REGISTRATION - Migrate routes to internal/router/ package + // ======================================================================== + routerConfig := &router.Config{ + Echo: e, + Queries: queries, + Cfg: cfg, + DBPool: dbPool, + AuthHandler: authHandler, + LibraryHandler: libraryHandler, + DeviceHandler: deviceHandler, + KOReaderHandler: koreaderHandler, + WSHandler: wsHandler, + ConflictHandler: conflictHandler, + AnalyticsHandler: analyticsHandler, + QueueHandler: queueHandler, + CollectionHandler: nil, // TODO: Initialize collection handler + OPDSHandler: opdsHandler, + ConnManager: connManager, + QueueProcessor: queueProcessor, + DeviceAuthMiddleware: deviceAuthMiddleware, + LoginTracker: loginAttemptTracker, + } + router.RegisterRoutes(routerConfig) + + // Public library types endpoint (no authentication required) + e.GET("/api/libraries/types", libraryHandler.GetLibraryTypes) + + // ======================================================================== + // FRONTEND ROUTES, HEALTH CHECK, DOCS (all now in router package) + // ======================================================================== + + // Start server + log.Printf("Starting server on port %s", cfg.ServerPort) + e.Logger.Fatal(e.Start(":" + cfg.ServerPort)) +} +``` + +**New Code:** + +```go +func main() { + cfg := config.LoadConfig() + + dbPool, err := pgxpool.New(context.Background(), cfg.DatabaseURL()) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + + queries := database.New(dbPool) + + // Create login attempt tracker: 5 failed attempts = 15 minute lockout + loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute) + + authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker) + libraryHandler := handlers.NewLibraryHandler(queries) + deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg) + deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries) + + // Create WebSocket connection manager + connManager := sync.NewConnectionManager() + connManager.StartCleanupTask() + + // Create sync queue processor + queueProcessor := sync.NewSyncQueueProcessor(queries) + go queueProcessor.Start(context.Background()) + + koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor) + wsHandler := handlers.NewWSHandler(queries, connManager, cfg.JWTSecret, deviceAuthMiddleware) + conflictHandler := handlers.NewConflictHandler(queries, connManager) + analyticsHandler := handlers.NewAnalyticsHandler(queries) + queueHandler := handlers.NewQueueHandler(queries, queueProcessor) + + // Create conversion service for EPUB→KEPUB conversion + conversionService := services.NewConversionService(queries, "/var/bookhoard/cache/kepub") + opdsHandler := handlers.NewOPDSHandler(queries, conversionService) + + e := echo.New() + + // Set up validator + v := validator.New() + + // Register custom password complexity validator + if err := ratelimit.RegisterPasswordValidation(v); err != nil { + log.Fatal("Failed to register password validator:", err) + } + e.Validator = &CustomValidator{validator: v} + + // Middleware + e.Use(echomiddleware.Logger()) + e.Use(echomiddleware.Recover()) + e.Use(echomiddleware.CORS()) + e.Use(ratelimit.RequestTracingMiddleware(cfg)) + + // ======================================================================== + // ROUTER REGISTRATION - Migrate routes to internal/router/ package + // ======================================================================== + routerConfig := &router.Config{ + Echo: e, + Queries: queries, + Cfg: cfg, + DBPool: dbPool, + AuthHandler: authHandler, + LibraryHandler: libraryHandler, + DeviceHandler: deviceHandler, + KOReaderHandler: koreaderHandler, + WSHandler: wsHandler, + ConflictHandler: conflictHandler, + AnalyticsHandler: analyticsHandler, + QueueHandler: queueHandler, + CollectionHandler: nil, // TODO: Initialize collection handler + OPDSHandler: opdsHandler, + ConnManager: connManager, + QueueProcessor: queueProcessor, + DeviceAuthMiddleware: deviceAuthMiddleware, + LoginTracker: loginAttemptTracker, + } + + // Register all routes and get ebook handler + ebookHandler := router.RegisterRoutes(routerConfig) + + // ======================================================================== + // APPLICATION LIFECYCLE MANAGEMENT + // ======================================================================== + application := app.New(e, ebookHandler, cfg, dbPool) + + // Start application in background + go func() { + if err := application.Start(); err != nil { + log.Fatal("Application error:", err) + } + }() + + // Wait for shutdown signal + application.WaitForShutdown() +} +``` + +**Key Changes:** +1. Capture ebookHandler return value from RegisterRoutes +2. Create app.Application instance +3. Start app in background goroutine +4. Call WaitForShutdown() to block +5. Graceful shutdown handled by app + +**Verification:** +- main.go reduced to setup code +- Application lifecycle managed by App +- Signal handling for graceful shutdown +- Background services auto-start + +--- + +## Phase 6: Fix Import in App File + +### File: `internal/app/app.go` + +**Add to imports:** + +```go +package app + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/labstack/echo/v4" + "bookhoard/internal/handlers" // ← ADD THIS +) +``` + +**Then update the App struct to use concrete type:** + +```go +type App struct { + Echo *echo.Echo + EbookHandler *handlers.Handler // ← CHANGE from interface{} to concrete type + Config *config.Config // ← CHANGE from interface{} to concrete type + DBPool *pgxpool.Pool // ← CHANGE from interface{} to concrete type + shutdownFuncs []func() error +} +``` + +**Add imports:** + +```go +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/jackc/pgx/v5/pgxpool" + "github.com/labstack/echo/v4" + "bookhoard/internal/config" // ← ADD THIS + "bookhoard/internal/handlers" // ← ADD THIS +) +``` + +**Update New function signature:** + +```go +func New(echo *echo.Echo, ebookHandler *handlers.Handler, cfg *config.Config, dbPool *pgxpool.Pool) *App { +``` + +**Update startBackgroundServices to use concrete type:** + +```go +func (a *App) startBackgroundServices() error { + log.Println("Starting background services...") + + // Start scheduler for auto-scanning + a.EbookHandler.StartScheduler() + + // Add shutdown function for scheduler + a.shutdownFuncs = append(a.shutdownFuncs, func() error { + a.EbookHandler.StopScheduler() + return nil + }) + + // Start watch mode for all libraries (with delay) + go func() { + time.Sleep(2 * time.Second) // Wait for server to be ready + if err := a.EbookHandler.StartWatchModeForAllLibraries(context.Background()); err != nil { + log.Printf("Warning: Failed to start watch mode: %v", err) + } + }() + + return nil +} +``` + +**Remove the callHandlerMethod function** - no longer needed with concrete types. + +**Verification:** +- All imports added +- Concrete types used instead of interface{} +- Direct method calls to EbookHandler +- Simpler, more maintainable code + +--- + +## Implementation Order (Step-by-Step) + +### Step 1: Create scanner routes (ADDITIVE ONLY) +- Create `internal/router/scanner.go` +- Zero risk - new file +- Does not affect existing code + +### Step 2: Update router (MINIMAL CHANGES) +- Capture EbookHandler in router.go +- Add to Config struct +- Call registerScannerRoutes +- Low risk - only adds route registration + +### Step 3: Implement library-type-aware scanner (ENHANCEMENT) +- Add libraryTypes field to EbookScanner +- Update SetFolders to build cache +- Replace isEbookFile with isScannableFile +- Medium risk - core scanner logic change +- TEST: Verify ebooks still scan correctly + +### Step 4: Create app package (ADDITIVE ONLY) +- Create `internal/app/app.go` +- Zero risk - new file +- Does not affect existing code + +### Step 5: Update main.go (REFACTOR) +- Replace initialization with App pattern +- Add signal handling +- Low risk - wraps existing code + +### Step 6: Test and Verify +- Run all existing routes +- Test new scanner endpoints +- Verify library type filtering +- Test graceful shutdown + +--- + +## Testing Checklist + +### After Each Phase: + +**Phase 1 (scanner.go created):** +- [ ] File created successfully +- [ ] No compilation errors + +**Phase 2 (router updated):** +- [ ] Code compiles +- [ ] All existing routes still accessible +- [ ] New scanner endpoints return 401 (auth required) or 404 (not implemented handlers) + +**Phase 3 (library-type-aware scanner):** +- [ ] Code compiles +- [ ] Ebook libraries scan ebooks only +- [ ] Comic/manga libraries would scan appropriate files +- [ ] No cross-contamination + +**Phase 4 (app package created):** +- [ ] File created successfully +- [ ] No compilation errors + +**Phase 5 (main.go updated):** +- [ ] Application starts successfully +- [ ] All existing routes work +- [ ] Server responds to requests +- [ ] Background services start (check logs) + +**Phase 6 (full system test):** +- [ ] Scanner endpoints accessible via curl +- [ ] Can trigger manual scan +- [ ] Scheduler starts (check logs) +- [ ] Watch mode starts (check logs after 2 seconds) +- [ ] SIGTERM/SIGINT triggers graceful shutdown +- [ ] All existing functionality still works + +--- + +## Verification Commands + +### Test existing routes still work: +```bash +# Test authentication +curl -X POST http://localhost:8765/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{"login":"testuser@example.com","password":"Test@Pass123!"}' + +# Test libraries +TOKEN= +curl http://localhost:8765/api/libraries/types +curl http://localhost:8765/api/libraries -H "Authorization: Bearer $TOKEN" +``` + +### Test new scanner endpoints: +```bash +# Test scan settings +curl http://localhost:8765/api/library/scan-settings -H "Authorization: Bearer $TOKEN" + +# Test scanner status (will return 404 if job doesn't exist) +curl http://localhost:8765/api/scanner/status/test-job-id -H "Authorization: Bearer $TOKEN" + +# Test watch mode status +curl http://localhost:8765/api/scanner/watch/status -H "Authorization: Bearer $TOKEN" +``` + +### Check logs for background services: +```bash +# Look for these log messages: +# "Starting scheduler for auto-scanning" +# "Scheduled scan for library" +# "Starting watch mode for all libraries" +# "Warning: failed to start watch mode" +``` + +### Test graceful shutdown: +```bash +# Start server, then send SIGTERM +kill -TERM + +# Or Ctrl+C which sends SIGINT +# Should see "Shutting down application..." message +``` + +--- + +## Rollback Plan + +If any phase breaks functionality: + +### Rollback Phase 2 (router changes): +```bash +git checkout internal/router/router.go +``` + +### Rollback Phase 3 (scanner changes): +```bash +git checkout internal/services/ebook_scanner.go +``` + +### Rollback Phase 5 (main.go changes): +```bash +git checkout cmd/server/main.go +``` + +### Rollback new files: +```bash +rm internal/router/scanner.go +rm internal/app/app.go +``` + +--- + +## Post-Implementation Improvements (Optional) + +These are NOT part of this plan but could be future enhancements: + +1. **Comic metadata extraction** + - Parse ComicInfo.xml from .cbz files + - Extract cover images from comic archives + - This is a separate feature + +2. **Scanner metrics** + - Track scan duration + - Count files per library type + - Error rates by file type + +3. **Scanner API improvements** + - Real-time scan progress via WebSocket + - Scan history/audit log + - Per-library scan schedules + +4. **Rename scanner** + - EbookScanner → MediaScanner or LibraryScanner + - Low priority, name doesn't affect functionality + +--- + +## Summary + +**What this restores:** +- ✅ 10 scanner endpoints (scan settings, control, watch mode) +- ✅ Background scheduler for auto-scanning +- ✅ Watch mode for instant ebook detection +- ✅ Library-type-aware scanning (prevents cross-contamination) +- ✅ Graceful shutdown with signal handling +- ✅ Application lifecycle management + +**What this doesn't break:** +- ✅ All existing routes (auth, library, device, media, collections, etc.) +- ✅ All existing API endpoints +- ✅ Database schema +- ✅ Any other functionality + +**Estimated implementation time:** 2-3 hours +**Risk level:** LOW +**Dependencies:** None (uses existing code) + +**Ready for implementation in next session.**