Files
bookhoard/SCANNER_RESTORATION_PLAN.md
T
john-okeefe 4b68ef8abd 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.
2026-02-06 21:59:30 -05:00

1117 lines
29 KiB
Markdown

# 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=<from login response>
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 <pid>
# 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.**